From 20d34102ff66f34e2c0bd528c35ac57ebf037f34 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 21 May 2026 16:42:23 -0400 Subject: [PATCH 001/120] Initial commit --- .../python_interface/dialects/qecl.py | 34 +++++++++++++++++-- .../dialects/test_qecl_dialect.py | 14 ++++++++ .../QecLogical/IR/QecLogicalAttrDefs.td | 4 +-- mlir/include/QecLogical/IR/QecLogicalOps.td | 34 +++++++++++++++++++ mlir/lib/QecLogical/IR/QecLogicalOps.cpp | 20 +++++++++++ mlir/test/QecLogical/DialectTest.mlir | 7 ++++ mlir/test/QecLogical/VerifierTest.mlir | 16 +++++++++ 7 files changed, 125 insertions(+), 4 deletions(-) diff --git a/frontend/catalyst/python_interface/dialects/qecl.py b/frontend/catalyst/python_interface/dialects/qecl.py index 0482233bac..028665c96e 100644 --- a/frontend/catalyst/python_interface/dialects/qecl.py +++ b/frontend/catalyst/python_interface/dialects/qecl.py @@ -65,8 +65,8 @@ class LogicalCodeblockInitState(StrEnum): """Enum for logical codeblock initial state""" - Zero = "zero" - # Add other supported codeblock initial states here + Zero = "zero" # |0⟩ + Magic = "magic" # |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) @irdl_attr_definition @@ -311,6 +311,35 @@ def __init__( ) +@irdl_op_definition +class FabricateOp(IRDLOperation): + """Fabricate a logical codeblock in a specified initial state.""" + + name = "qecl.fabricate" + + assembly_format = """ + `[` $init_state `]` attr-dict `:` type($out_codeblock) + """ + + init_state = prop_def(LogicalCodeblockInitStateAttr) + + out_codeblock = result_def(base(LogicalCodeblockType)) + + def __init__( + self, + init_state: str | LogicalCodeblockInitStateAttr, + out_codeblock_type: LogicalCodeblockType, + ): + init_state_attr = ( + init_state + if isinstance(init_state, LogicalCodeblockInitStateAttr) + else LogicalCodeblockInitStateAttr(init_state) + ) + properties = {"init_state": init_state_attr} + + super().__init__(result_types=(out_codeblock_type,), properties=properties) + + @irdl_op_definition class EncodeOp(IRDLOperation): """Encode a logical codeblock to the specified logical state.""" @@ -748,6 +777,7 @@ def __init__( DeallocOp, ExtractCodeblockOp, InsertCodeblockOp, + FabricateOp, EncodeOp, NoiseOp, QecCycleOp, diff --git a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py index 4e84df1e34..85009c5b9d 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py @@ -61,6 +61,7 @@ def create_ssa_value(t: AttributeCovT) -> OpResult[AttributeCovT]: "DeallocOp": "qecl.dealloc", "ExtractCodeblockOp": "qecl.extract_block", "InsertCodeblockOp": "qecl.insert_block", + "FabricateOp": "qecl.fabricate", "EncodeOp": "qecl.encode", "NoiseOp": "qecl.noise", "QecCycleOp": "qecl.qec", @@ -194,6 +195,16 @@ def test_qecl_op_constructor_insert_block(self, idx, assert_valid_idx_attr): assert_valid_idx_attr(insert_block_op, idx) + @pytest.mark.parametrize("init_state", ["magic", qecl.LogicalCodeblockInitStateAttr("magic")]) + def test_qecl_op_constructor_fabricate(self, init_state): + """Test the constructor of the qecl.fabricate op.""" + fabricate_op = qecl.FabricateOp( + init_state=init_state, out_codeblock_type=qecl.LogicalCodeblockType(k=self.k) + ) + assert len(fabricate_op.result_types) == 1 + assert isinstance(fabricate_op.result_types[0], qecl.LogicalCodeblockType) + assert fabricate_op.result_types[0].k == self.k + @pytest.mark.parametrize("init_state", ["zero", qecl.LogicalCodeblockInitStateAttr("zero")]) def test_qecl_op_constructor_encode(self, init_state): """Test the constructor of the qecl.encode op.""" @@ -301,6 +312,9 @@ def test_assembly_format(run_filecheck, pretty_print): // CHECK: qecl.insert_block [[hyperreg]][{{\s*}}0], [[block0]] : !qecl.hyperreg<3 x 1>, !qecl.codeblock<1> %hreg1 = qecl.insert_block %hyperreg[ 0], %block0 : !qecl.hyperreg<3 x 1>, !qecl.codeblock<1> + // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic] : !qecl.codeblock<1> + %magic = qecl.fabricate [magic] : !qecl.codeblock<1> + // CHECK: [[block1:%.+]] = qecl.encode{{\s*}}[zero] [[block0]] : !qecl.codeblock<1> %block1 = qecl.encode [zero] %block0 : !qecl.codeblock<1> diff --git a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td index 4e597b623b..d6a3596487 100644 --- a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td +++ b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td @@ -26,8 +26,8 @@ include "QecLogical/IR/QecLogicalDialect.td" def LogicalCodeblockInitState : I32EnumAttr<"LogicalCodeblockInitState", "logical codeblock initial state", [ - I32EnumAttrCase<"Zero", 0, "zero"> // |0⟩ - // Add other supported codeblock initial states here + I32EnumAttrCase<"Zero", 0, "zero">, // |0⟩ + I32EnumAttrCase<"Magic", 1, "magic"> // |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) ]> { let cppNamespace = "catalyst::qecl"; let genSpecializedAttr = 0; diff --git a/mlir/include/QecLogical/IR/QecLogicalOps.td b/mlir/include/QecLogical/IR/QecLogicalOps.td index fe40ca3419..819059e913 100644 --- a/mlir/include/QecLogical/IR/QecLogicalOps.td +++ b/mlir/include/QecLogical/IR/QecLogicalOps.td @@ -107,6 +107,38 @@ def InsertCodeblockOp : QecLogical_Op<"insert_block", [AllTypesMatch<["in_hyper_ let hasFolder = 1; } +def FabricateOp : QecLogical_Op<"fabricate"> { + let summary = "Fabricate a logical codeblock in a specified initial state."; + + let description = [{ + This operation represents the fabrication of a single logical codeblock in a specified + initial logical state. The process of "fabrication" is distinct from standard codeblock + allocation; specifically, it represents the retrieval of k logical qubits, which constitute + a single logical codeblock, from a qubit factory, where the target initial state is + typically expensive to produce. + + This operation is most commonly used in the context of magic-state injection for the + application of T gates or π/8 Pauli-product rotations (PPR) in the Pauli-based computation + (PBC) framework. Consequently, this operation follows the naming conventions of the + `pbc.fabricate` operation for more seamless integration between the `pbc` and `qecl` + compilation layers. + }]; + + let arguments = (ins + LogicalCodeblockInitStateAttr:$init_state + ); + + let results = (outs + LogicalCodeblockType:$out_codeblock + ); + + let assemblyFormat = [{ + `[` $init_state `]` attr-dict `:` qualified(type($out_codeblock)) + }]; + + let hasVerifier = true; +} + def EncodeOp : QecLogical_Op<"encode", [AllTypesMatch<["in_codeblock", "out_codeblock"]>]> { let summary = "Encode a logical codeblock to the specified logical state."; @@ -122,6 +154,8 @@ def EncodeOp : QecLogical_Op<"encode", [AllTypesMatch<["in_codeblock", "out_code let assemblyFormat = [{ `[` $init_state `]` $in_codeblock attr-dict `:` qualified(type($in_codeblock)) }]; + + let hasVerifier = true; } def NoiseOp : QecLogical_Op<"noise", [AllTypesMatch<["in_codeblock", "out_codeblock"]>]> { diff --git a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp index a8d7c3f365..12627777d5 100644 --- a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp +++ b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp @@ -92,6 +92,26 @@ LogicalResult InsertCodeblockOp::verify() return success(); } +LogicalResult FabricateOp::verify() +{ + auto initState = getInitState(); + if (initState == LogicalCodeblockInitState::Zero) { + return emitOpError() << "cannot fabricate a codeblock in the logical 'zero' state, use '" + << EncodeOp::getOperationName() << "' instead."; + } + return success(); +} + +LogicalResult EncodeOp::verify() +{ + auto initState = getInitState(); + if (initState == LogicalCodeblockInitState::Magic) { + return emitOpError() << "cannot encode a logical codeblock to the magic state, use '" + << FabricateOp::getOperationName() << "' instead."; + } + return success(); +} + template static LogicalResult verifySingleQubitLogicalGateOp(OpTy op) { if (!(op.getIdx() || op.getIdxAttr().has_value())) { diff --git a/mlir/test/QecLogical/DialectTest.mlir b/mlir/test/QecLogical/DialectTest.mlir index f9f55240f3..8850aa98b7 100644 --- a/mlir/test/QecLogical/DialectTest.mlir +++ b/mlir/test/QecLogical/DialectTest.mlir @@ -69,6 +69,13 @@ func.func @test_insert_block_dyn_idx(%arg0 : !qecl.hyperreg<3 x 1>, %arg1 : inde // ----- +func.func @test_fabricate() { + %0 = qecl.fabricate [magic] : !qecl.codeblock<1> + func.return +} + +// ----- + func.func @test_encode_block(%arg0 : !qecl.codeblock<1>) { %0 = qecl.encode [zero] %arg0 : !qecl.codeblock<1> func.return diff --git a/mlir/test/QecLogical/VerifierTest.mlir b/mlir/test/QecLogical/VerifierTest.mlir index 8281c3554a..e8701c4484 100644 --- a/mlir/test/QecLogical/VerifierTest.mlir +++ b/mlir/test/QecLogical/VerifierTest.mlir @@ -78,6 +78,22 @@ func.func @test_insert_type_mismatch(%r : !qecl.hyperreg<3 x 1>, %b : !qecl.code // ----- +func.func @test_fabricate_zero() { + // expected-error@below {{cannot fabricate a codeblock in the logical 'zero' state}} + %0 = qecl.fabricate [zero] : !qecl.codeblock<1> + func.return +} + +// ----- + +func.func @test_encode_magic(%arg0 : !qecl.codeblock<1>) { + // expected-error@below {{cannot encode a logical codeblock to the magic state}} + %0 = qecl.encode [magic] %arg0 : !qecl.codeblock<1> + func.return +} + +// ----- + func.func @test_gate_op_index_out_of_bounds_identity(%b : !qecl.codeblock<1>) { // expected-error@below {{out-of-bounds index attribute}} %b1 = qecl.identity %b[1] : !qecl.codeblock<1> From f9c548fd71cbf3fe374261f61139d31d671a4596 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 21 May 2026 16:49:09 -0400 Subject: [PATCH 002/120] Add changelog entry --- doc/releases/changelog-dev.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 53c2d24fc7..a9ef26a2bf 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -93,6 +93,13 @@ dialect. They are now accessible as `mbqc.ref.measure_in_basis` and `mbqc.ref.graph_state_prep`. [(#2829)](https://github.com/PennyLaneAI/catalyst/pull/2829) +* In order to support T gates in the experimental QEC pipeline, the following new operations have + been added: + + - `qecl.fabricate`, which fabricates a logical codeblock in a specified initial state (typically a + magic state). + [(#2865)](https://github.com/PennyLaneAI/catalyst/pull/2865) +

Documentation 📝

Contributors ✍️

From 3d05162fcfac9fc7ea10ce617156e0904837fa5c Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Fri, 22 May 2026 10:20:19 -0400 Subject: [PATCH 003/120] Initial commit --- .../catalyst/python_interface/dialects/qecl.py | 17 +++++++++++++++++ .../dialects/test_qecl_dialect.py | 9 +++++++++ mlir/include/QecLogical/IR/QecLogicalOps.td | 12 ++++++++++++ mlir/test/QecLogical/DialectTest.mlir | 7 +++++++ 4 files changed, 45 insertions(+) diff --git a/frontend/catalyst/python_interface/dialects/qecl.py b/frontend/catalyst/python_interface/dialects/qecl.py index 028665c96e..0e64b73bee 100644 --- a/frontend/catalyst/python_interface/dialects/qecl.py +++ b/frontend/catalyst/python_interface/dialects/qecl.py @@ -340,6 +340,22 @@ def __init__( super().__init__(result_types=(out_codeblock_type,), properties=properties) +@irdl_op_definition +class DeallocCodeblockOp(IRDLOperation): + """Deallocate a single logical codeblock.""" + + name = "qecl.dealloc_cb" + + assembly_format = """ + $codeblock attr-dict `:` type($codeblock) + """ + + codeblock = operand_def(base(LogicalCodeblockType)) + + def __init__(self, codeblock: LogicalCodeBlockSSAValue | Operation): + super().__init__(operands=(codeblock,)) + + @irdl_op_definition class EncodeOp(IRDLOperation): """Encode a logical codeblock to the specified logical state.""" @@ -778,6 +794,7 @@ def __init__( ExtractCodeblockOp, InsertCodeblockOp, FabricateOp, + DeallocCodeblockOp, EncodeOp, NoiseOp, QecCycleOp, diff --git a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py index 85009c5b9d..0a06ba7b6b 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py @@ -63,6 +63,7 @@ def create_ssa_value(t: AttributeCovT) -> OpResult[AttributeCovT]: "InsertCodeblockOp": "qecl.insert_block", "FabricateOp": "qecl.fabricate", "EncodeOp": "qecl.encode", + "DeallocCodeblockOp": "qecl.dealloc_cb", "NoiseOp": "qecl.noise", "QecCycleOp": "qecl.qec", "IdentityOp": "qecl.identity", @@ -205,6 +206,11 @@ def test_qecl_op_constructor_fabricate(self, init_state): assert isinstance(fabricate_op.result_types[0], qecl.LogicalCodeblockType) assert fabricate_op.result_types[0].k == self.k + def test_qecl_op_constructor_dealloc_cb(self): + """Test the constructor of the qecl.dealloc_cb op.""" + dealloc_cb_op = qecl.DeallocCodeblockOp(self._get_codeblock_value()) + assert len(dealloc_cb_op.result_types) == 0 + @pytest.mark.parametrize("init_state", ["zero", qecl.LogicalCodeblockInitStateAttr("zero")]) def test_qecl_op_constructor_encode(self, init_state): """Test the constructor of the qecl.encode op.""" @@ -315,6 +321,9 @@ def test_assembly_format(run_filecheck, pretty_print): // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic] : !qecl.codeblock<1> %magic = qecl.fabricate [magic] : !qecl.codeblock<1> + // CHECK: qecl.dealloc_cb [[magic]] : !qecl.codeblock<1> + qecl.dealloc_cb %magic : !qecl.codeblock<1> + // CHECK: [[block1:%.+]] = qecl.encode{{\s*}}[zero] [[block0]] : !qecl.codeblock<1> %block1 = qecl.encode [zero] %block0 : !qecl.codeblock<1> diff --git a/mlir/include/QecLogical/IR/QecLogicalOps.td b/mlir/include/QecLogical/IR/QecLogicalOps.td index 819059e913..e47a2c35fa 100644 --- a/mlir/include/QecLogical/IR/QecLogicalOps.td +++ b/mlir/include/QecLogical/IR/QecLogicalOps.td @@ -139,6 +139,18 @@ def FabricateOp : QecLogical_Op<"fabricate"> { let hasVerifier = true; } +def DeallocCodeblockOp : QecLogical_Op<"dealloc_cb"> { + let summary = "Deallocate a single logical codeblock."; + + let arguments = (ins + LogicalCodeblockType:$codeblock + ); + + let assemblyFormat = [{ + $codeblock attr-dict `:` qualified(type($codeblock)) + }]; +} + def EncodeOp : QecLogical_Op<"encode", [AllTypesMatch<["in_codeblock", "out_codeblock"]>]> { let summary = "Encode a logical codeblock to the specified logical state."; diff --git a/mlir/test/QecLogical/DialectTest.mlir b/mlir/test/QecLogical/DialectTest.mlir index 8850aa98b7..746df9fbae 100644 --- a/mlir/test/QecLogical/DialectTest.mlir +++ b/mlir/test/QecLogical/DialectTest.mlir @@ -76,6 +76,13 @@ func.func @test_fabricate() { // ----- +func.func @test_dealloc_cb(%arg0 : !qecl.codeblock<1>) { + qecl.dealloc_cb %arg0 : !qecl.codeblock<1> + func.return +} + +// ----- + func.func @test_encode_block(%arg0 : !qecl.codeblock<1>) { %0 = qecl.encode [zero] %arg0 : !qecl.codeblock<1> func.return From 6f954285f55b0dd33d5b92a2e908b16d5ec543cf Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Fri, 22 May 2026 10:22:49 -0400 Subject: [PATCH 004/120] Add changelog entry --- doc/releases/changelog-dev.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 7c6e3c7342..b412c67915 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -102,6 +102,8 @@ - `qecl.fabricate`, which fabricates a logical codeblock in a specified initial state (typically a magic state). [(#2865)](https://github.com/PennyLaneAI/catalyst/pull/2865) + - `qecl.dealloc_cb`, which deallocates a single logical codeblock. + [(#2866)](https://github.com/PennyLaneAI/catalyst/pull/2866)

Documentation 📝

From 61da46f4e9f1e53ea5f75d92e861cd3c9b3b4d9b Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 17:02:53 -0400 Subject: [PATCH 005/120] add initial prototype --- .../qecl/convert_quantum_to_qecl.py | 103 +++++++++++++++++- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 27 +++++ 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 39a1b548a0..fa3145b93c 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -83,9 +83,9 @@ from xdsl.builder import ImplicitBuilder from xdsl.context import Context -from xdsl.dialects import arith, builtin, scf -from xdsl.dialects.builtin import IndexType, IntegerAttr, IntegerType -from xdsl.ir import Block, BlockArgument, Operation, SSAValue +from xdsl.dialects import arith, builtin, func, scf +from xdsl.dialects.builtin import IndexType, IntegerAttr, IntegerType, SymbolRefAttr +from xdsl.ir import Block, BlockArgument, Operation, Region, SSAValue from xdsl.passes import ModulePass from xdsl.pattern_rewriter import ( GreedyRewritePatternApplier, @@ -345,7 +345,9 @@ def match_and_rewrite(self, op: quantum.DeallocOp, rewriter: PatternRewriter): class CustomOpConversion(RewritePattern): - """Converts `quantum.custom` ops to equivalent `qecl.hadamard`, `qecl.s` and `qecl.cnot` ops. + """Converts `quantum.custom` ops for Clifford+T and Pauli gates to their equivalent `qecl` + ops. The gates "S", "Hadamard", "CNOT", "PauliX", "PauliY", "PauliZ" and "Identity" have + corresponding `qecl` operators. "T" gates are lowered to a subroutine, based on ToDo: CITE PAPER HERE. For now, we insert cycles of QEC after every gate operation. @@ -368,6 +370,23 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): case "Identity" | "PauliX" | "PauliY" | "PauliZ" | "Hadamard" | "S": ops_to_insert = self._get_qecl_ops_for_single_qubit_gate(op) + case "T": + qubit_owner_op = op.in_qubits[0].owner + if not _is_type_convertible(qubit_owner_op, qecl.LogicalCodeblockType): + _raise_failed_to_convert_op_compile_error(op) + ops_to_insert = ( + conv_cast_op := builtin.UnrealizedConversionCastOp.get( + (qubit_owner_op.results[0],), (qubit_owner_op.operands[0].type,) + ), + t_gate_subroutine := func.CallOp( + callee=SymbolRefAttr("apply_T"), + arguments=conv_cast_op.results[0], + return_types=conv_cast_op.results[0].type, + ), + qec_cycle_op := qecl.QecCycleOp(in_codeblock=t_gate_subroutine.results[0]), + _cast_to_qubit(qec_cycle_op.out_codeblock), + ) + case "CNOT": assert len(op.in_qubits) == 2 ctrl_qubit_owner_op = op.in_qubits[0].owner @@ -403,11 +422,10 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): trgt_conv_cast_op := _cast_to_qubit(trgt_qecl_cycle_op.out_codeblock), ) new_results = (ctrl_conv_cast_op.results[0], trgt_conv_cast_op.results[0]) - case _: raise CompileError( f"Conversion of op '{op.name}' only supports gates 'Identity', 'PauliX', " - f"'PauliY', 'PauliZ', 'Hadamard', 'S' and 'CNOT', but got '{gate_name}'" + f"'PauliY', 'PauliZ', 'Hadamard', 'S', 'T' and 'CNOT', but got '{gate_name}'" ) rewriter.replace_op(op, ops_to_insert, new_results=new_results) @@ -599,6 +617,11 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: f"codeblock, k, is 1, but got k = {self.k}" ) + module_block = op.regions[0].blocks.first + assert module_block is not None, "Module has no block" + t_subroutine = self.create_t_subroutine() + module_block.add_op(t_subroutine) + PatternRewriteWalker( GreedyRewritePatternApplier( [ @@ -616,5 +639,73 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: # this pass removes them ReconcileUnrealizedCastsPass().apply(ctx, op) + def create_t_subroutine(self): + """ToDo: docstring""" + + codeblock_type = qecl.LogicalCodeblockType(self.k) + input_types = (codeblock_type,) + output_types = (codeblock_type,) + + block = Block(arg_types=input_types) + + with ImplicitBuilder(block): + (in_codeblock,) = block.args + + # allocate auxiliary codeblock + # this will be replaced with magic state fabrication op + aux_cb_register = qecl.AllocOp(qecl.LogicalHyperRegisterType(width=1, k=self.k)) + extract_op = qecl.ExtractCodeblockOp(hyper_reg=aux_cb_register.results[0], idx=0) + magic_state_block = extract_op.results[0] + + # apply cnot between aux block and data block + cnot_op = qecl.CnotOp( + in_ctrl_codeblock=magic_state_block, + idx_ctrl=0, + in_trgt_codeblock=in_codeblock, + idx_trgt=0, + ) + magic_state1, codeblock1 = cnot_op.results + meas_op = qecl.MeasureOp(codeblock1, idx=0) + mres, finished_codeblock = meas_op.results + + # will be replaced with the dealloc op + insert_op = qecl.InsertCodeblockOp( + in_hyper_reg=aux_cb_register, idx=0, codeblock=finished_codeblock + ) + qecl.DeallocOp(insert_op.results[0]) + + # corrections + # if mres, apply S, if mres, apply X? ToDo: verify procedure + if_apply_corr_op = scf.IfOp( + mres, + return_types=(in_codeblock.type,), + true_region=Region(Block()), + false_region=Region(Block()), + ) + + with ImplicitBuilder(if_apply_corr_op.true_region): + # This branch is for the case where a correction is needed + corrected_cb1 = qecl.SOp(magic_state1, idx=0) + corr_cb_out = qecl.PauliXOp(corrected_cb1, idx=0) + scf.YieldOp(corr_cb_out) + + with ImplicitBuilder(if_apply_corr_op.false_region): + # This branch is for the case where no correctable error was detected + scf.YieldOp(magic_state1) + + # return the encoded codeblock + func.ReturnOp( + if_apply_corr_op.results[0], + ) + + funcOp = func.FuncOp( + name=f"apply_T", + function_type=(input_types, output_types), + visibility="private", + region=Region([block]), + ) + + return funcOp + convert_quantum_to_qecl_pass = compiler_transform(ConvertQuantumToQecLogicalPass) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index dffeb60aec..6f5afa9a07 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -633,6 +633,33 @@ def test_gate_s_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """ run_filecheck(program, quantum_to_qecl_pipeline_k_1) + def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): + """Test that T gates (`quantum.custom "T"() ops) are converted to their corresponding + `apply_T` subroutine for k = 1. + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecl.codeblock<1> + // CHECK-NOT: builtin.unrealized_conversion_cast + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = builtin.unrealized_conversion_cast %0 : !qecl.codeblock<1> to !quantum.bit + + // CHECK: [[cb1:%.+]] = func.call @apply_T([[cb0]]) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + // CHECK: [[cb2:%.+]] = qecl.qec [[cb1]] : !qecl.codeblock<1> + %2 = quantum.custom "T"() %1 : !quantum.bit + + // CHECK: [[conv_cast:%.+]] = builtin.unrealized_conversion_cast [[cb2]] : !qecl.codeblock<1> to !quantum.bit + // CHECK: "test.op"([[conv_cast]]) : (!quantum.bit) -> !quantum.bit + %3 = "test.op"(%2) : (!quantum.bit) -> !quantum.bit // To prevent DCE + return + } + // CHECK: func.func private @apply_T([[codeblock:%.+]]: !qecl.codeblock<1>) + } + """ + run_filecheck(program, quantum_to_qecl_pipeline_k_1) + # ToDo: make check for apply_T subroutine more complete + def test_gate_cnot_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """Test that CNOT gates (`quantum.custom "CNOT"() ops) are converted to their corresponding `qecl.cnot` ops for k = 1. From a7e5ace93e652a6135955774721ad825e02a03b1 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 17:10:23 -0400 Subject: [PATCH 006/120] use fabricate and dealloc_cb ops --- .../qecl/convert_quantum_to_qecl.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index fa3145b93c..d79c0b120b 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -651,13 +651,11 @@ def create_t_subroutine(self): with ImplicitBuilder(block): (in_codeblock,) = block.args - # allocate auxiliary codeblock - # this will be replaced with magic state fabrication op - aux_cb_register = qecl.AllocOp(qecl.LogicalHyperRegisterType(width=1, k=self.k)) - extract_op = qecl.ExtractCodeblockOp(hyper_reg=aux_cb_register.results[0], idx=0) - magic_state_block = extract_op.results[0] + # fabricate aux codeblock in magic state + fabricate_op = qecl.FabricateOp("magic", qecl.LogicalCodeblockType(k=self.k)) + magic_state_block = fabricate_op.results[0] - # apply cnot between aux block and data block + # apply cnot between aux codeblock and input data codeblock cnot_op = qecl.CnotOp( in_ctrl_codeblock=magic_state_block, idx_ctrl=0, @@ -665,16 +663,16 @@ def create_t_subroutine(self): idx_trgt=0, ) magic_state1, codeblock1 = cnot_op.results + + # measure original data codeblock meas_op = qecl.MeasureOp(codeblock1, idx=0) mres, finished_codeblock = meas_op.results - # will be replaced with the dealloc op - insert_op = qecl.InsertCodeblockOp( - in_hyper_reg=aux_cb_register, idx=0, codeblock=finished_codeblock - ) - qecl.DeallocOp(insert_op.results[0]) + # dealloate the original data codeblock + # data is now on aux codeblock (up to a correction) + qecl.DeallocCodeblockOp(finished_codeblock) - # corrections + # apply corrections based on measurement outcome # if mres, apply S, if mres, apply X? ToDo: verify procedure if_apply_corr_op = scf.IfOp( mres, From 6b251cfa6f9f024404dd762943a6de8e5f41bf98 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 17:25:28 -0400 Subject: [PATCH 007/120] update test --- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index 6f5afa9a07..00c6a51c4d 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -654,11 +654,21 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): %3 = "test.op"(%2) : (!quantum.bit) -> !quantum.bit // To prevent DCE return } - // CHECK: func.func private @apply_T([[codeblock:%.+]]: !qecl.codeblock<1>) + // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecl.codeblock<1>) + // CHECK-NEXT: [[magic_cb:%.+]] = qecl.fabricate[magic] : !qecl.codeblock<1> + // CHECK-NEXT: [[magic_cb2:%.+]], [[in_codeblock2:%.+]] = qecl.cnot [[magic_cb]][0], [[in_codeblock]][0] + // CHECK-NEXT: [[mres:%.+]], [[in_codeblock3:%.+]] = qecl.measure [[in_codeblock2]][0] + // CHECK-NEXT: qecl.dealloc_cb [[in_codeblock3]] + // CHECK-NEXT: [[out_codeblock:%.+]] = scf.if [[mres]] -> (!qecl.codeblock<1>) + // CHECK-NEXT: [[s_corrected_cb:%.+]] = qecl.s [[magic_cb2]][0] + // CHECK-NEXT: [[corrected_cb:%.+]] = qecl.x [[s_corrected_cb]] + // CHECK-NEXT: scf.yield [[corrected_cb]] + // CHECK-NEXT: else + // CHECK-NEXT: scf.yield [[magic_cb2]] + // CHECK: func.return [[out_codeblock]] } """ run_filecheck(program, quantum_to_qecl_pipeline_k_1) - # ToDo: make check for apply_T subroutine more complete def test_gate_cnot_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """Test that CNOT gates (`quantum.custom "CNOT"() ops) are converted to their corresponding From 3dc17cb2b9016c904e13fbc98cd2e43bc0085b6a Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 17:37:54 -0400 Subject: [PATCH 008/120] add T gate to integration test --- .../transforms/qecl/test_xdsl_convert_quantum_to_qecl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index 00c6a51c4d..c7aec543fb 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -872,12 +872,14 @@ def circuit(): # CHECK: qecl.qec # CHECK: qecl.hadamard {{%.+}}[0] # CHECK: qecl.qec + # CHECK: apply_T # CHECK: qecl.measure {{%.+}}[0] # CHECK: quantum.mcmobs # CHECK: quantum.sample # CHECK: qecl.insert_block # CHECK: qecl.dealloc qp.H(0) + qp.T(0) m0 = qp.measure(0) return qp.sample([m0]) From bbf7d64816c77a2ee05307a79ea97d08831012e0 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 17:50:19 -0400 Subject: [PATCH 009/120] update docstrings and comments --- .../qecl/convert_quantum_to_qecl.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index d79c0b120b..42dd0d996a 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -345,9 +345,9 @@ def match_and_rewrite(self, op: quantum.DeallocOp, rewriter: PatternRewriter): class CustomOpConversion(RewritePattern): - """Converts `quantum.custom` ops for Clifford+T and Pauli gates to their equivalent `qecl` - ops. The gates "S", "Hadamard", "CNOT", "PauliX", "PauliY", "PauliZ" and "Identity" have - corresponding `qecl` operators. "T" gates are lowered to a subroutine, based on ToDo: CITE PAPER HERE. + """Converts `quantum.custom` ops for Clifford+T and Pauli gates to their equivalent `qecl` + ops. The gates "S", "Hadamard", "CNOT", "PauliX", "PauliY", "PauliZ" and "Identity" have + corresponding `qecl` operators. "T" gates are lowered to a subroutine. For now, we insert cycles of QEC after every gate operation. @@ -640,7 +640,23 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: ReconcileUnrealizedCastsPass().apply(ctx, op) def create_t_subroutine(self): - """ToDo: docstring""" + """Create a subroutine that takes in a codeblock in state |φ>, and outputs a codeblock + in state T|φ>. The subroutine includes instructions to fabricate a codeblock in the + magic state, entangle it with the input codeblock and perform measurement and corrections. + As the gate application includes teleportation of the state from the input codeblock to + the codeblock containing the magic state, the output codeblock is the one generated by + the fabricate op. + + This procedure is outlined in Nielsen & Chuang, (Section 10.6.2), and is as follows: + + |magic_state> ──╭●───────SX─┤ output_state + | ║ + |input_state> ──╰X──┤↗├──║── deallocate + ╚═══╝ + + Note that this method does not insert the subroutine into the module op. Instead it returns + the built func.FuncOp object that can then be subsequently inserted where desired. + """ codeblock_type = qecl.LogicalCodeblockType(self.k) input_types = (codeblock_type,) @@ -672,8 +688,7 @@ def create_t_subroutine(self): # data is now on aux codeblock (up to a correction) qecl.DeallocCodeblockOp(finished_codeblock) - # apply corrections based on measurement outcome - # if mres, apply S, if mres, apply X? ToDo: verify procedure + # apply correction based on measurement outcome if_apply_corr_op = scf.IfOp( mres, return_types=(in_codeblock.type,), From e665d8b7f2bb5f3a8347b003b858a5b47d0ca87e Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Fri, 22 May 2026 22:05:20 -0400 Subject: [PATCH 010/120] Apply suggestion from @lillian542 --- .../python_interface/transforms/qecl/convert_quantum_to_qecl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 42dd0d996a..b2c844a722 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -649,7 +649,7 @@ def create_t_subroutine(self): This procedure is outlined in Nielsen & Chuang, (Section 10.6.2), and is as follows: - |magic_state> ──╭●───────SX─┤ output_state + |magic_state> ──╭●───────SX─┤ |output_state> | ║ |input_state> ──╰X──┤↗├──║── deallocate ╚═══╝ From c1e61a8da2479b92f7c63679324b5eda50a63d50 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 22 May 2026 22:09:17 -0400 Subject: [PATCH 011/120] formatting --- .../python_interface/transforms/qecl/convert_quantum_to_qecl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index b2c844a722..73ab8c6e0e 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -422,6 +422,7 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): trgt_conv_cast_op := _cast_to_qubit(trgt_qecl_cycle_op.out_codeblock), ) new_results = (ctrl_conv_cast_op.results[0], trgt_conv_cast_op.results[0]) + case _: raise CompileError( f"Conversion of op '{op.name}' only supports gates 'Identity', 'PauliX', " From 9859931408b1c3d9d7a8763721f16125a50e3443 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 25 May 2026 14:58:18 -0400 Subject: [PATCH 012/120] add changelog --- doc/releases/changelog-dev.md | 4 ++++ .../transforms/qecl/convert_quantum_to_qecl.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 91672eef1c..69bae28631 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -72,6 +72,10 @@ Physical (`qecp`) dialect. [(#2776)](https://github.com/PennyLaneAI/catalyst/pull/2776) +* The experimental compiler pass `convert-quantum-to-qecl` has been extended to lower + `quantum.custom "T"` gates to the `qecl` layer as a subroutine using a magic state. + [(#2870)](https://github.com/PennyLaneAI/catalyst/pull/2870) + * The reference semantics Pauli Product Measurement operation `pbc.ref.ppm` was added. [(#2773)](https://github.com/PennyLaneAI/catalyst/pull/2773) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 73ab8c6e0e..540e95bd99 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -713,7 +713,7 @@ def create_t_subroutine(self): ) funcOp = func.FuncOp( - name=f"apply_T", + name="apply_T", function_type=(input_types, output_types), visibility="private", region=Region([block]), From 5aefacb843d3a4b2dd7e4edbadfc480b431af4ea Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Tue, 26 May 2026 13:19:07 -0400 Subject: [PATCH 013/120] Apply suggestions from code review Co-authored-by: Joey Carter --- .../transforms/qecl/convert_quantum_to_qecl.py | 2 +- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 540e95bd99..35d3dcb945 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -643,7 +643,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: def create_t_subroutine(self): """Create a subroutine that takes in a codeblock in state |φ>, and outputs a codeblock in state T|φ>. The subroutine includes instructions to fabricate a codeblock in the - magic state, entangle it with the input codeblock and perform measurement and corrections. + magic state, entangle it with the input codeblock and perform measurements and corrections. As the gate application includes teleportation of the state from the input codeblock to the codeblock containing the magic state, the output codeblock is the one generated by the fabricate op. diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index c7aec543fb..b4c71c7e34 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -654,18 +654,18 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): %3 = "test.op"(%2) : (!quantum.bit) -> !quantum.bit // To prevent DCE return } - // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecl.codeblock<1>) + // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecl.codeblock<1>) // CHECK-NEXT: [[magic_cb:%.+]] = qecl.fabricate[magic] : !qecl.codeblock<1> // CHECK-NEXT: [[magic_cb2:%.+]], [[in_codeblock2:%.+]] = qecl.cnot [[magic_cb]][0], [[in_codeblock]][0] // CHECK-NEXT: [[mres:%.+]], [[in_codeblock3:%.+]] = qecl.measure [[in_codeblock2]][0] // CHECK-NEXT: qecl.dealloc_cb [[in_codeblock3]] // CHECK-NEXT: [[out_codeblock:%.+]] = scf.if [[mres]] -> (!qecl.codeblock<1>) - // CHECK-NEXT: [[s_corrected_cb:%.+]] = qecl.s [[magic_cb2]][0] - // CHECK-NEXT: [[corrected_cb:%.+]] = qecl.x [[s_corrected_cb]] - // CHECK-NEXT: scf.yield [[corrected_cb]] + // CHECK-NEXT: [[s_corrected_cb:%.+]] = qecl.s [[magic_cb2]][0] + // CHECK-NEXT: [[corrected_cb:%.+]] = qecl.x [[s_corrected_cb]] + // CHECK-NEXT: scf.yield [[corrected_cb]] // CHECK-NEXT: else - // CHECK-NEXT: scf.yield [[magic_cb2]] - // CHECK: func.return [[out_codeblock]] + // CHECK-NEXT: scf.yield [[magic_cb2]] + // CHECK: func.return [[out_codeblock]] } """ run_filecheck(program, quantum_to_qecl_pipeline_k_1) From 4221d7cf4e3b27e1890f5ebdd6c46a6eff3f5dd7 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Tue, 26 May 2026 13:20:37 -0400 Subject: [PATCH 014/120] Apply suggestion from @lillian542 --- .../python_interface/transforms/qecl/convert_quantum_to_qecl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 35d3dcb945..b5bf73b6b4 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -673,6 +673,7 @@ def create_t_subroutine(self): magic_state_block = fabricate_op.results[0] # apply cnot between aux codeblock and input data codeblock + # hardcode idx=0 for now, based on the current assumption that k=1 cnot_op = qecl.CnotOp( in_ctrl_codeblock=magic_state_block, idx_ctrl=0, From 964a0e1c3b08a6fa1156b7a059f73349d8367c2c Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 26 May 2026 13:51:44 -0400 Subject: [PATCH 015/120] pass subroutine to pattern --- .../transforms/qecl/convert_quantum_to_qecl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index b5bf73b6b4..87c58873e2 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -343,7 +343,7 @@ def match_and_rewrite(self, op: quantum.DeallocOp, rewriter: PatternRewriter): # MARK: Custom Op Pattern - +@dataclass class CustomOpConversion(RewritePattern): """Converts `quantum.custom` ops for Clifford+T and Pauli gates to their equivalent `qecl` ops. The gates "S", "Hadamard", "CNOT", "PauliX", "PauliY", "PauliZ" and "Identity" have @@ -359,6 +359,8 @@ class CustomOpConversion(RewritePattern): quantum-to-qecl dialect conversion supports arbitrary values of k >= 1. """ + t_subroutine: func.FuncOp + @op_type_rewrite_pattern def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): """Rewrite pattern for `quantum.custom` ops.""" @@ -379,7 +381,7 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): (qubit_owner_op.results[0],), (qubit_owner_op.operands[0].type,) ), t_gate_subroutine := func.CallOp( - callee=SymbolRefAttr("apply_T"), + callee=SymbolRefAttr(self.t_subroutine.sym_name), arguments=conv_cast_op.results[0], return_types=conv_cast_op.results[0].type, ), @@ -630,7 +632,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: ExtractOpConversion(), InsertOpConversion(), DeallocOpConversion(), - CustomOpConversion(), + CustomOpConversion(t_subroutine=t_subroutine), MeasureOpConversion(), ] ) From be890e1e94fb3c04d02aa674d3a16fb24850779e Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 27 May 2026 10:40:26 -0400 Subject: [PATCH 016/120] black formatting --- .../python_interface/transforms/qecl/convert_quantum_to_qecl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 87c58873e2..d164d382e8 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -343,6 +343,7 @@ def match_and_rewrite(self, op: quantum.DeallocOp, rewriter: PatternRewriter): # MARK: Custom Op Pattern + @dataclass class CustomOpConversion(RewritePattern): """Converts `quantum.custom` ops for Clifford+T and Pauli gates to their equivalent `qecl` From 620c9b71f73fc939071853fd3430f55ee23874f2 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 27 May 2026 12:29:37 -0400 Subject: [PATCH 017/120] add temporary dce in tests after pass --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 376f9c9032..3d77d1fb37 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -558,6 +558,7 @@ def test_qec_pass_ghz_lightning_integration(self, run_filecheck_qjit): @convert_qecp_to_quantum_pass @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass + @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(1) @qp.qnode(dev, mcm_method="one-shot") @@ -611,6 +612,7 @@ def test_qec_pass_ghz_nullqubit_integration(self): @convert_qecp_to_quantum_pass @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass + @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(10) @qp.qnode(dev, mcm_method="one-shot") From 269e3d4698272c44da8e572df9828a48af5f6aa0 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 27 May 2026 13:50:14 -0400 Subject: [PATCH 018/120] add more temporary dce in tests --- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index bc5dd5ad18..127fa8472b 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1028,6 +1028,7 @@ def test_circuit_ghz_to_qecp(self, run_filecheck_qjit): @qp.qjit(capture=True, target="mlir") @convert_qecl_to_qecp_pass(qec_code="Steane") + @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.qnode(dev, shots=1) def circuit(): @@ -1075,6 +1076,7 @@ def test_convert_qecl_noise_to_qecp_noise_pass_integration(self, run_filecheck_q @qp.qjit(target="mlir", capture=True) @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass + @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.qnode(dev, shots=1) def circuit(): From 3c6b42255bebadcd088191966f7902b6b1b99c6b Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 14:34:20 -0400 Subject: [PATCH 019/120] add outline of magic state subroutine --- .../transforms/qecp/convert_qecl_to_qecp.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index d9843790cb..083bf4e6f3 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -743,6 +743,63 @@ def create_encode_subroutine(self) -> func.FuncOp: ) return funcOp + + + # MARK: Magic state subroutine + + + def create_fabricate_magic_subroutine(self) -> func.FuncOp: + """Create a subroutine that allocates a codeblock and encodes it in the magic state for + the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a + non-fault tolerant encoding intended for use on a simulator, and not a distillation process + for generating a magic state from many noisy copies. + + The encoding process involves putting the initial QEC physical qubit in the desired state, + and then using the same encoding procedure used for encoding the zero state, following the + example shown in arXiv: 0905.2794, Section VIII.A. + + The subroutine allocates auxiliary qubits for use in encoding based on the number of + rows in the X tanner graph, and deallocates them once encoding is complete. + + Note that this method does not insert the subroutine into the module op. Instead it returns + the built func.FuncOp object that can then be subsequently inserted where desired. + """ + codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) + input_types = () + output_types = (codeblock_type,) + + block = Block(arg_types=input_types) + tanner_x, tanner_z = self.insert_tanner_graph_ops_into_block(block) + + with ImplicitBuilder(block): + codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType) + + # apply H and T to the first qubit + # ToDo: does it matter which one I apply it to? If so, can I deduce the correct answer from the matrix for the tanner graph? Experiment in a notebook. + initial_qubit = qecp.ExtractQubitOp(codeblock, 0) + physical_h = qecp.HadamardOp(initial_qubit) + # ToDo: add T to qecp physical dialect + # physical_t = qecp. + codeblock = qecp.InsertQubitOp(codeblock, 0) + + # Apply X checks + Z correction pattern + x_out_codeblock = self._qec_cycle_css_pattern(codeblock, CheckType.X, tanner_x) + + # Apply Z checks + X correction pattern + z_out_codeblock = self._qec_cycle_css_pattern(x_out_codeblock, CheckType.Z, tanner_z) + + # return the encoded codeblock + func.ReturnOp(z_out_codeblock) + + funcOp = func.FuncOp( + name=f"fabricate_magic_state_{self.qec_code.name}", + function_type=(input_types, output_types), + visibility="private", + region=Region([block]), + ) + + return funcOp + # MARK: 1Q gate subroutines From 91bb2790e2138d75406d34c31bcbcdc0b3c7bd5b Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 14:59:23 -0400 Subject: [PATCH 020/120] add MLIR op and tests --- mlir/include/QecPhysical/IR/QecPhysicalOps.td | 4 ++++ mlir/test/QecPhysical/DialectTest.mlir | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/mlir/include/QecPhysical/IR/QecPhysicalOps.td b/mlir/include/QecPhysical/IR/QecPhysicalOps.td index 04a560d8ee..8dd0772f73 100644 --- a/mlir/include/QecPhysical/IR/QecPhysicalOps.td +++ b/mlir/include/QecPhysical/IR/QecPhysicalOps.td @@ -278,6 +278,10 @@ def HadamardOp : SingleQubitPhysicalGate_Op<"hadamard", [AllTypesMatch<["in_qubi let summary = "A physical Hadamard gate operation."; } +def TOp : SingleQubitPhysicalGate_Op<"t", [AllTypesMatch<["in_qubit", "out_qubit"]>]> { + let summary = "A physical T gate operation."; +} + def SOp : SingleQubitPhysicalGate_Op<"s", [AllTypesMatch<["in_qubit", "out_qubit"]>]> { let summary = "A physical S (π/2 phase) gate operation."; } diff --git a/mlir/test/QecPhysical/DialectTest.mlir b/mlir/test/QecPhysical/DialectTest.mlir index f8452ff4d9..afbdfec892 100644 --- a/mlir/test/QecPhysical/DialectTest.mlir +++ b/mlir/test/QecPhysical/DialectTest.mlir @@ -177,6 +177,14 @@ func.func @test_gate_op_hadamard(%arg0 : !qecp.qubit, %arg1 : !qecp.qubit< // ----- +func.func @test_gate_op_t(%arg0 : !qecp.qubit, %arg1 : !qecp.qubit) { + %0 = qecp.t %arg0 : !qecp.qubit + %1 = qecp.t %arg1 : !qecp.qubit + func.return +} + +// ----- + func.func @test_gate_op_s(%arg0 : !qecp.qubit, %arg1 : !qecp.qubit) { %0 = qecp.s %arg0 : !qecp.qubit %1 = qecp.s %0 adj : !qecp.qubit From 810b1eb97cec8901fd85aef50f9bc879f860ef61 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 14:59:37 -0400 Subject: [PATCH 021/120] add xdsl Op and tests --- .../python_interface/dialects/qecp.py | 7 +++ .../dialects/test_qecp_dialect.py | 54 +++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/frontend/catalyst/python_interface/dialects/qecp.py b/frontend/catalyst/python_interface/dialects/qecp.py index 4a98ac26fb..2d0ee3230e 100644 --- a/frontend/catalyst/python_interface/dialects/qecp.py +++ b/frontend/catalyst/python_interface/dialects/qecp.py @@ -682,6 +682,13 @@ class SOp(SingleQubitPhysicalGateOp): name = "qecp.s" +@irdl_op_definition +class TOp(SingleQubitPhysicalGateOp): + """A physical T gate operation.""" + + name = "qecp.t" + + @irdl_op_definition class CnotOp(IRDLOperation): """A physical CNOT gate operation.""" diff --git a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py index 32413e7d18..6bfa9390d2 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py @@ -82,6 +82,7 @@ def create_ssa_value(t: AttributeCovT) -> OpResult[AttributeCovT]: "PauliZOp": "qecp.z", "HadamardOp": "qecp.hadamard", "SOp": "qecp.s", + "TOp": "qecp.t", "RotOp": "qecp.rot", "CnotOp": "qecp.cnot", "MeasureOp": "qecp.measure", @@ -332,6 +333,22 @@ def test_qecp_op_constructor_hadamard(self, qubit): assert len(hadamard_op.result_types) == 1 assert hadamard_op.result_types[0] == qubit.type + @pytest.mark.parametrize( + "qubit", + [ + create_ssa_value(qecp.QecPhysicalQubitType("data")), + create_ssa_value(qecp.QecPhysicalQubitType("aux")), + ], + ) + def test_qecp_op_constructor_t(self, qubit): + """Test the constructor of the qecp.t op.""" + t_op = qecp.TOp(qubit) + assert len(t_op.operands) == 1 + assert t_op.operand_types[0] == qubit.type + assert len(t_op.result_types) == 1 + assert t_op.result_types[0] == qubit.type + + @pytest.mark.parametrize( "qubit", [ @@ -535,26 +552,31 @@ def test_assembly_format(run_filecheck, pretty_print): %qd5 = qecp.hadamard %qd4 : !qecp.qubit %qa5 = qecp.hadamard %qa4 : !qecp.qubit - // CHECK: [[qd6:%.+]] = qecp.s [[qd5]] : !qecp.qubit - // CHECK: [[qa6:%.+]] = qecp.s [[qa5]] : !qecp.qubit - %qd6 = qecp.s %qd5 : !qecp.qubit - %qa6 = qecp.s %qa5 : !qecp.qubit + // CHECK: [[qd6:%.+]] = qecp.t [[qd5]] : !qecp.qubit + // CHECK: [[qa6:%.+]] = qecp.t [[qa5]] : !qecp.qubit + %qd6 = qecp.t %qd5 : !qecp.qubit + %qa6 = qecp.t %qa5 : !qecp.qubit + + // CHECK: [[qd7:%.+]] = qecp.s [[qd6]] : !qecp.qubit + // CHECK: [[qa7:%.+]] = qecp.s [[qa6]] : !qecp.qubit + %qd7 = qecp.s %qd6 : !qecp.qubit + %qa7 = qecp.s %qa6 : !qecp.qubit - // CHECK: [[qd7:%.+]] = qecp.s [[qd6]] adj : !qecp.qubit - // CHECK: [[qa7:%.+]] = qecp.s [[qa6]] adj : !qecp.qubit - %qd7 = qecp.s %qd6 adj : !qecp.qubit - %qa7 = qecp.s %qa6 adj : !qecp.qubit + // CHECK: [[qd8:%.+]] = qecp.s [[qd7]] adj : !qecp.qubit + // CHECK: [[qa8:%.+]] = qecp.s [[qa7]] adj : !qecp.qubit + %qd8 = qecp.s %qd7 adj : !qecp.qubit + %qa8 = qecp.s %qa7 adj : !qecp.qubit // CHECK: [[phi:%.+]] = "test.op"() : () -> f64 // CHECK: [[theta:%.+]] = "test.op"() : () -> f64 // CHECK: [[omega:%.+]] = "test.op"() : () -> f64 - // CHECK: [[qd8:%.+]] = qecp.rot([[phi:%.+]], [[theta:%.+]], [[omega:%.+]]) [[qd7]] : !qecp.qubit - // CHECK: [[qa8:%.+]] = qecp.rot([[phi:%.+]], [[theta:%.+]], [[omega:%.+]]) [[qa7]] : !qecp.qubit + // CHECK: [[qd9:%.+]] = qecp.rot([[phi:%.+]], [[theta:%.+]], [[omega:%.+]]) [[qd8]] : !qecp.qubit + // CHECK: [[qa9:%.+]] = qecp.rot([[phi:%.+]], [[theta:%.+]], [[omega:%.+]]) [[qa8]] : !qecp.qubit %phi = "test.op"() : () -> f64 %theta = "test.op"() : () -> f64 %omega = "test.op"() : () -> f64 - %qd8 = qecp.rot(%phi, %theta, %omega) %qd7 : !qecp.qubit - %qa8 = qecp.rot(%phi, %theta, %omega) %qa7 : !qecp.qubit + %qd9 = qecp.rot(%phi, %theta, %omega) %qd8 : !qecp.qubit + %qa9 = qecp.rot(%phi, %theta, %omega) %qa8 : !qecp.qubit // CHECK: [[qd10:%.+]] = "test.op"() : () -> !qecp.qubit // CHECK: [[qd20:%.+]] = "test.op"() : () -> !qecp.qubit @@ -582,10 +604,10 @@ def test_assembly_format(run_filecheck, pretty_print): %row_idx = "test.op"() : () -> tensor<8xi32> %col_ptr = "test.op"() : () -> tensor<6xi32> - // CHECK: [[mres0:%.+]], [[qd9:%.+]] = qecp.measure [[qd8]] : i1, !qecp.qubit - // CHECK: [[mres1:%.+]], [[qa9:%.+]] = qecp.measure [[qa8]] : i1, !qecp.qubit - %mres0, %qd9 = qecp.measure %qd8 : i1, !qecp.qubit - %mres1, %qa9 = qecp.measure %qa8 : i1, !qecp.qubit + // CHECK: [[mres0:%.+]], [[qd10:%.+]] = qecp.measure [[qd9]] : i1, !qecp.qubit + // CHECK: [[mres1:%.+]], [[qa10:%.+]] = qecp.measure [[qa9]] : i1, !qecp.qubit + %mres0, %qd10 = qecp.measure %qd9 : i1, !qecp.qubit + %mres1, %qa10 = qecp.measure %qa9 : i1, !qecp.qubit // CHECK: [[tgraph:%.+]] = qecp.assemble_tanner [[row_idx]], [[col_ptr]] : tensor<8xi32>, tensor<6xi32> -> !qecp.tanner_graph<8, 6, i32> %tgraph = qecp.assemble_tanner %row_idx, %col_ptr : tensor<8xi32>, tensor<6xi32> -> !qecp.tanner_graph<8, 6, i32> From 57c5cdd5c895de5360778ed79b9492c2c1c2bc64 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 15:39:19 -0400 Subject: [PATCH 022/120] add to dialect list --- frontend/catalyst/python_interface/dialects/qecp.py | 4 ++++ .../pytest/python_interface/dialects/test_qecp_dialect.py | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/dialects/qecp.py b/frontend/catalyst/python_interface/dialects/qecp.py index 2d0ee3230e..8996e23b21 100644 --- a/frontend/catalyst/python_interface/dialects/qecp.py +++ b/frontend/catalyst/python_interface/dialects/qecp.py @@ -686,6 +686,9 @@ class SOp(SingleQubitPhysicalGateOp): class TOp(SingleQubitPhysicalGateOp): """A physical T gate operation.""" + def __init__(self, in_qubit: QecPhysicalQubitSSAValue | Operation): + super().__init__(in_qubit) + name = "qecp.t" @@ -932,6 +935,7 @@ def __init__( PauliYOp, PauliZOp, HadamardOp, + TOp, RotOp, SOp, CnotOp, diff --git a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py index 6bfa9390d2..cad3caaedf 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py @@ -348,7 +348,6 @@ def test_qecp_op_constructor_t(self, qubit): assert len(t_op.result_types) == 1 assert t_op.result_types[0] == qubit.type - @pytest.mark.parametrize( "qubit", [ From ffb49e83ee7aee9ae2d4ba17441d79d1f9faf0ce Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 16:21:53 -0400 Subject: [PATCH 023/120] fix duplicated ssa in test --- .../pytest/python_interface/dialects/test_qecp_dialect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py index cad3caaedf..377aa04291 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecp_dialect.py @@ -605,8 +605,8 @@ def test_assembly_format(run_filecheck, pretty_print): // CHECK: [[mres0:%.+]], [[qd10:%.+]] = qecp.measure [[qd9]] : i1, !qecp.qubit // CHECK: [[mres1:%.+]], [[qa10:%.+]] = qecp.measure [[qa9]] : i1, !qecp.qubit - %mres0, %qd10 = qecp.measure %qd9 : i1, !qecp.qubit - %mres1, %qa10 = qecp.measure %qa9 : i1, !qecp.qubit + %mres0, %qd13 = qecp.measure %qd9 : i1, !qecp.qubit + %mres1, %qa13 = qecp.measure %qa9 : i1, !qecp.qubit // CHECK: [[tgraph:%.+]] = qecp.assemble_tanner [[row_idx]], [[col_ptr]] : tensor<8xi32>, tensor<6xi32> -> !qecp.tanner_graph<8, 6, i32> %tgraph = qecp.assemble_tanner %row_idx, %col_ptr : tensor<8xi32>, tensor<6xi32> -> !qecp.tanner_graph<8, 6, i32> From 7cbcf497e1f99383a91f01186d0d8c829b1ffb22 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 28 May 2026 16:29:52 -0400 Subject: [PATCH 024/120] add changelog --- doc/releases/changelog-dev.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 020ac735bc..87fcc4b140 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -125,6 +125,8 @@ [(#2867)](https://github.com/PennyLaneAI/catalyst/pull/2867) - `qecp.dealloc_cb`, which deallocates a single physical codeblock. [(#2867)](https://github.com/PennyLaneAI/catalyst/pull/2867) + - `qecp.t`, which performs a T gate on a single physical qubit. + [(#2888)](https://github.com/PennyLaneAI/catalyst/pull/2888) From a0a5d43093f3e7b5c5edb73d4976d0918869ea48 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 10:53:36 -0400 Subject: [PATCH 025/120] add lowering for qecp.t, dealloc_cb, alloc_cb --- .../qecp/convert_qecp_to_quantum.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index a9fd8ef867..ce7a1d9d93 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -54,6 +54,7 @@ "qecp.hadamard": "Hadamard", "qecp.identity": "Identity", "qecp.s": "S", + "qecp.t": "T", "qecp.x": "PauliX", "qecp.y": "PauliY", "qecp.z": "PauliZ", @@ -123,7 +124,7 @@ def convert_type( return QubitType() -# MARK: Auxiliary qubit Alloc/Dealloc Patterns +# MARK: Alloc/Dealloc Patterns @dataclass(frozen=True) @@ -145,6 +146,24 @@ def match_and_rewrite(self, op: qecp.DeallocAuxQubitOp, rewriter: PatternRewrite """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary qubit.""" rewriter.replace_op(op, quantum.DeallocQubitOp(op.qubit)) +@dataclass(frozen=True) +class AllocCodeblockConversion(RewritePattern): + """Op conversion pattern from qecp.alloc_cb to quantum.alloc.""" + + @op_type_rewrite_pattern + def match_and_rewrite(self, op: qecp.AllocCodeblockOp, rewriter: PatternRewriter): + """Op conversion rewrite pattern for lowering ops that allocate an auxiliary qubit.""" + rewriter.replace_op(op, quantum.AllocOp(op.codeblock.type.n)) + + +@dataclass(frozen=True) +class DeallocCodeblockConversion(RewritePattern): + """Op conversion pattern from qecp.dealloc_cb to quantum.alloc.""" + + @op_type_rewrite_pattern + def match_and_rewrite(self, op: qecp.DeallocCodeblockOp, rewriter: PatternRewriter): + """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary qubit.""" + rewriter.replace_op(op, quantum.DeallocOp(op.codeblock)) # MARK: Data qubit extract and insertion patterns @@ -356,6 +375,8 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: PatternRewriteWalker( GreedyRewritePatternApplier( [ + AllocCodeblockConversion(), + DeallocCodeblockConversion(), PhysicalCodeblockTypeConversion(recursive=True), QecPhysicalQubitTypeConversion(recursive=True), AllocAuxQubitConversion(), From 475df5801b291169d50e897b8fac419d427118dc Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 10:53:59 -0400 Subject: [PATCH 026/120] remove temporary dce passes --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 3d77d1fb37..376f9c9032 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -558,7 +558,6 @@ def test_qec_pass_ghz_lightning_integration(self, run_filecheck_qjit): @convert_qecp_to_quantum_pass @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(1) @qp.qnode(dev, mcm_method="one-shot") @@ -612,7 +611,6 @@ def test_qec_pass_ghz_nullqubit_integration(self): @convert_qecp_to_quantum_pass @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(10) @qp.qnode(dev, mcm_method="one-shot") From 67b12250adc389644e9442ebf0764a4c7614bf22 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 11:00:12 -0400 Subject: [PATCH 027/120] add lowering patterns --- .../transforms/qecp/convert_qecl_to_qecp.py | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 083bf4e6f3..27ca0c2ffd 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -140,6 +140,16 @@ def match_and_rewrite(self, op: qecl.DeallocOp, rewriter: PatternRewriter): rewriter.replace_op(op, qecp.DeallocOp(op.hyper_reg)) +@dataclass +class DeallocCodeBlockConversion(RewritePattern): + """Op conversion pattern from qecl.dealloc_cb -> qecp.dealloc_cb.""" + + @op_type_rewrite_pattern + def match_and_rewrite(self, op: qecl.DeallocCodeblockOp, rewriter: PatternRewriter): + """Op conversion rewrite pattern for lowering ops that allocate codeblocks.""" + rewriter.replace_op(op, qecp.DeallocCodeblockOp(op.codeblock)) + + # MARK: Extract/Insert Patterns @@ -363,6 +373,49 @@ def match_and_rewrite( rewriter.replace_op(op, subroutine_call_op) +# MARK: Apply_T Pattern + +@dataclass(frozen=True) +class ApplyTConversion(RewritePattern): + """Converts qecl.fabricate [magic] to the equivalent subroutine of qecp gates""" + + qec_code: QecCode + fabricate_subroutine: func.FuncOp + + @op_type_rewrite_pattern + def match_and_rewrite(self, funcop: func.FuncOp, rewriter: PatternRewriter): + """Rewrite pattern for the `apply_T` subroutine""" + + if not funcop.sym_name.data == "apply_T": + return + + for op in funcop.body.walk(): + if isinstance(op, qecl.FabricateOp): + + if not op.init_state.data == "magic": + raise NotImplementedError( + "Lowering qecl.FabricateOp to the qecp dialect is only implemented " + "for init_state 'magic'" + ) + + out_codeblock = cast( + qecl.LogicalCodeBlockSSAValue | qecp.PhysicalCodeBlockSSAValue, op.out_codeblock + ) + + if (k := out_codeblock.type.k.value.data) != self.qec_code.k: + raise CompileError( + f"Circuit expressed in the qecl dialect with k={k} is not compatible with " + f"lowering to a code with k={self.qec_code.k}" + ) + + callee = builtin.SymbolRefAttr(self.fabricate_subroutine.sym_name) + return_types = self.fabricate_subroutine.function_type.outputs.data + callOp = func.CallOp(callee, arguments=(), return_types=return_types) + rewriter.replace_op(op, callOp) + + funcop.update_function_type() + + # MARK: Conversion Pass @@ -425,6 +478,9 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine() module_block.add_op(physical_meas_decode_subroutine) + fabricate_subroutine = self.create_fabricate_subroutine() + module_block.add_op(fabricate_subroutine) + # 1Q gate and 2Q gate subroutines are returned as dicts storing # {"gate_name": subroutine_funcop} transversal_gate_subroutines = ( @@ -441,9 +497,11 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: HyperRegisterTypeConversion(qec_code=self.qec_code), AllocationConversion(), DeallocationConversion(), + DeallocCodeBlockConversion(), InsertBlockConversion(), ExtractBlockConversion(), EncodeOpConversion(qec_code=self.qec_code, encode_subroutine=encode_subroutine), + ApplyTConversion(qec_code=self.qec_code, fabricate_subroutine=fabricate_subroutine), QecCycleOpConversion( qec_code=self.qec_code, qec_cycle_subroutine=qec_cycle_subroutine ), @@ -745,10 +803,10 @@ def create_encode_subroutine(self) -> func.FuncOp: return funcOp - # MARK: Magic state subroutine + # MARK: Fabricate subroutine - def create_fabricate_magic_subroutine(self) -> func.FuncOp: + def create_fabricate_subroutine(self) -> func.FuncOp: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a non-fault tolerant encoding intended for use on a simulator, and not a distillation process @@ -772,15 +830,14 @@ def create_fabricate_magic_subroutine(self) -> func.FuncOp: tanner_x, tanner_z = self.insert_tanner_graph_ops_into_block(block) with ImplicitBuilder(block): - codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType) + codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n)) # apply H and T to the first qubit # ToDo: does it matter which one I apply it to? If so, can I deduce the correct answer from the matrix for the tanner graph? Experiment in a notebook. initial_qubit = qecp.ExtractQubitOp(codeblock, 0) physical_h = qecp.HadamardOp(initial_qubit) - # ToDo: add T to qecp physical dialect - # physical_t = qecp. - codeblock = qecp.InsertQubitOp(codeblock, 0) + physical_t = qecp.TOp(physical_h.results[0]) + codeblock = qecp.InsertQubitOp(codeblock, 0, physical_t.results[0]) # Apply X checks + Z correction pattern x_out_codeblock = self._qec_cycle_css_pattern(codeblock, CheckType.X, tanner_x) From 87889ab7aaa8be69e439245d2769a5d5c5692d04 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 11:00:36 -0400 Subject: [PATCH 028/120] start adding tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 127fa8472b..bea17fd5b6 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1015,6 +1015,38 @@ def test_nontransveral_ops_ignored(self, run_filecheck): run_filecheck(program, pipeline) +# Mark: ApplyT + +class TestLoweringApplyT: + + def test_apply_t_toy_code(self, run_filecheck): + pass + #ToDo + + def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): + """Test that the apply_T subroutine is lowered as expected for the Steane code. + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 7> + %0 = "test.op"() : () -> !qecl.codeblock<1> + + // CHECK: [[cb1:%.+]] = func.call @apply_T([[cb0]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> + %2 = func.call @apply_T(%0) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + return + } + // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) + func.func private @apply_T(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { + %1 = qecl.fabricate[magic] : !qecl.codeblock<1> + qecl.dealloc_cb %0 : !qecl.codeblock<1> + func.return %1 : !qecl.codeblock<1> + } + } + """ + run_filecheck(program, qecl_to_qecp_steane_pipeline) + raise RuntimeError() + # MARK: Integration @@ -1028,7 +1060,6 @@ def test_circuit_ghz_to_qecp(self, run_filecheck_qjit): @qp.qjit(capture=True, target="mlir") @convert_qecl_to_qecp_pass(qec_code="Steane") - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.qnode(dev, shots=1) def circuit(): @@ -1076,7 +1107,6 @@ def test_convert_qecl_noise_to_qecp_noise_pass_integration(self, run_filecheck_q @qp.qjit(target="mlir", capture=True) @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.qnode(dev, shots=1) def circuit(): From d0bd82d8db23a2d5a7dd1b0261947e0f0be1d498 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 16:02:49 -0400 Subject: [PATCH 029/120] reverse order of s and x --- .../transforms/qecl/convert_quantum_to_qecl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index d164d382e8..7ef2e628b9 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -703,8 +703,8 @@ def create_t_subroutine(self): with ImplicitBuilder(if_apply_corr_op.true_region): # This branch is for the case where a correction is needed - corrected_cb1 = qecl.SOp(magic_state1, idx=0) - corr_cb_out = qecl.PauliXOp(corrected_cb1, idx=0) + corrected_cb1 = qecl.PauliXOp(magic_state1, idx=0) + corr_cb_out = qecl.SOp(corrected_cb1, idx=0) scf.YieldOp(corr_cb_out) with ImplicitBuilder(if_apply_corr_op.false_region): From ac6ba58c59f98e9af4054fa9f8cb7840234408a4 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 17:40:37 -0400 Subject: [PATCH 030/120] fix deallocation bug --- .../transforms/qecp/convert_qecp_to_quantum.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index a9fd8ef867..7218338fc5 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -284,6 +284,8 @@ def match_and_rewrite(self, op: scf.ForOp, rewriter: PatternRewriter): rewriter.erase_op(op) +# MARK: Conversion Pass + @dataclass(frozen=True) class ConvertQecPhysicalToQuantumPass(ModulePass): """ @@ -335,8 +337,7 @@ def _apply_experimental_hyperregister_lowering(self, op: builtin.ModuleOp): quantum_op.codeblock.replace_all_uses_with(regs[idx]) case qecp.InsertCodeblockOp(): qecp_ops_to_remove.append(quantum_op) - idx = resolve_constant_params(quantum_op.idx) - dealloc = quantum.DeallocOp(regs[idx]) + dealloc = quantum.DeallocOp(quantum_op.codeblock) rewriter.insert_op(dealloc) quantum_op.results[0].replace_all_uses_with(qecp_alloc_op.results[0]) case qecp.DeallocOp(): @@ -345,6 +346,7 @@ def _apply_experimental_hyperregister_lowering(self, op: builtin.ModuleOp): for quantum_op in reversed(qecp_ops_to_remove): rewriter = PatternRewriter(quantum_op) rewriter.erase_op(quantum_op) + # Remove dead code region_dce(op_.body) From 226f558147b945fb4cba3e837e67ba99077da72a Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 17:52:05 -0400 Subject: [PATCH 031/120] update test to catch bug --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 3d77d1fb37..aa84533551 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -502,15 +502,15 @@ def test_hyperregister_lowering(self, run_filecheck): func.func public @circuit() -> () attributes {quantum.node} { // CHECK-NOT: qecp // CHECK: [[reg0:%.+]] = quantum.alloc(7) : !quantum.reg - // CHECK-NEXT: [[reg0:%.+]] = func.call @encode_zero_Steane([[reg0:%.+]]) : (!quantum.reg) -> !quantum.reg + // CHECK-NEXT: [[reg0_1:%.+]] = func.call @encode_zero_Steane([[reg0]]) : (!quantum.reg) -> !quantum.reg // CHECK-NEXT: [[reg1:%.+]] = quantum.alloc(7) : !quantum.reg - // CHECK-NEXT: [[reg1:%.+]] = func.call @encode_zero_Steane([[reg0:%.+]]) : (!quantum.reg) -> !quantum.reg - // CHECK: [[reg0:%.+]] = func.call @noise_subroutine_code_1x7x1([[reg0:%.+]] - // CHECK-NEXT: [[reg0:%.+]] = func.call @qec_cycle_Steane([[reg0:%.+]]) : (!quantum.reg) -> !quantum.reg - // CHECK-NEXT: [[reg0:%.+]] = func.call @hadamard_Steane([[reg0:%.+]]) : (!quantum.reg) -> !quantum.reg - // CHECK-NEXT: [[reg1:%.+]] = func.call @hadamard_Steane([[reg1:%.+]]) : (!quantum.reg) -> !quantum.reg - // CHECK-NEXT: quantum.dealloc [[reg0:%.+]] : !quantum.reg - // CHECK-NEXT: quantum.dealloc [[reg1:%.+]] : !quantum.reg + // CHECK-NEXT: [[reg1_1:%.+]] = func.call @encode_zero_Steane([[reg1]]) : (!quantum.reg) -> !quantum.reg + // CHECK: [[reg0_2:%.+]] = func.call @noise_subroutine_code_1x7x1([[reg0_1]] + // CHECK-NEXT: [[reg0_3:%.+]] = func.call @qec_cycle_Steane([[reg0_2]]) : (!quantum.reg) -> !quantum.reg + // CHECK-NEXT: [[reg0_4:%.+]] = func.call @hadamard_Steane([[reg0_3]]) : (!quantum.reg) -> !quantum.reg + // CHECK-NEXT: [[reg1_2:%.+]] = func.call @hadamard_Steane([[reg1_1]]) : (!quantum.reg) -> !quantum.reg + // CHECK-NEXT: quantum.dealloc [[reg0_4]] : !quantum.reg + // CHECK-NEXT: quantum.dealloc [[reg1_2]] : !quantum.reg // CHECK-NEXT: quantum.device_release %0 = qecp.alloc() : !qecp.hyperreg<2 x 1 x 7> %1 = arith.constant 0 : index From 0994c0adb314f3b6d651d3b48d3d9b3014109b42 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 18:04:18 -0400 Subject: [PATCH 032/120] black formatting --- .../python_interface/transforms/qecp/convert_qecp_to_quantum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index 7218338fc5..4bc6b06ab0 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -286,6 +286,7 @@ def match_and_rewrite(self, op: scf.ForOp, rewriter: PatternRewriter): # MARK: Conversion Pass + @dataclass(frozen=True) class ConvertQecPhysicalToQuantumPass(ModulePass): """ From 9c160b1ebb8e88627bffc5cc583b0a3116abb4f3 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 23:40:51 -0400 Subject: [PATCH 033/120] always use for-loop for encoding --- .../qecl/convert_quantum_to_qecl.py | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index d164d382e8..0f5fc23efe 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -170,59 +170,48 @@ def _get_hyper_reg_encoding_ops( ops_to_insert: tuple[Operation, ...] = () - if hyper_reg_width == 1: - # No need to loop, insert encode op directly. - ops_to_insert = ( - extract_op := qecl.ExtractCodeblockOp(hyper_reg=hyper_reg, idx=0), - encode_op := qecl.EncodeOp(extract_op.codeblock, init_state="zero"), - qecl.InsertCodeblockOp( - in_hyper_reg=hyper_reg, idx=0, codeblock=encode_op.out_codeblock - ), - ) + # Loop over all codeblocks in the hyper-register and encode them to logical zero state. + # Ops for lower bound, upper bound, and step size. + lb_op = arith.ConstantOp.from_int_and_width(0, IndexType()) + ub_op = arith.ConstantOp.from_int_and_width(hyper_reg_width, IndexType()) + step_op = arith.ConstantOp.from_int_and_width(1, IndexType()) + + for_body = Block( + [], + arg_types=(builtin.IndexType(), hyper_reg.type), + ) - else: - # Loop over all codeblocks in the hyper-register and encode them to logical zero state. - # Ops for lower bound, upper bound, and step size. - lb_op = arith.ConstantOp.from_int_and_width(0, IndexType()) - ub_op = arith.ConstantOp.from_int_and_width(hyper_reg_width, IndexType()) - step_op = arith.ConstantOp.from_int_and_width(1, IndexType()) - - for_body = Block( - [], - arg_types=(builtin.IndexType(), hyper_reg.type), - ) + for_each_codeblock_op = scf.ForOp( + lb=lb_op, + ub=ub_op, + step=step_op, + iter_args=(hyper_reg,), + body=for_body, + ) - for_each_codeblock_op = scf.ForOp( - lb=lb_op, - ub=ub_op, - step=step_op, - iter_args=(hyper_reg,), - body=for_body, + # Build the body of the for loop. On each iteration, extract the codeblock at the + # iteration index, encode it, and re-insert into hyper-register. Finally, yield the + # updated hyper-register. + with ImplicitBuilder(for_each_codeblock_op.body): + indvar = cast(BlockArgument[IndexType], for_each_codeblock_op.body.block.args[0]) + hyper_reg = cast( + BlockArgument[qecl.LogicalHyperRegisterType], + for_each_codeblock_op.body.block.args[1], ) - # Build the body of the for loop. On each iteration, extract the codeblock at the - # iteration index, encode it, and re-insert into hyper-register. Finally, yield the - # updated hyper-register. - with ImplicitBuilder(for_each_codeblock_op.body): - indvar = cast(BlockArgument[IndexType], for_each_codeblock_op.body.block.args[0]) - hyper_reg = cast( - BlockArgument[qecl.LogicalHyperRegisterType], - for_each_codeblock_op.body.block.args[1], - ) - - extract_op = qecl.ExtractCodeblockOp(hyper_reg=hyper_reg, idx=indvar) - encode_op = qecl.EncodeOp(extract_op.codeblock, init_state="zero") - insert_op = qecl.InsertCodeblockOp( - in_hyper_reg=hyper_reg, idx=indvar, codeblock=encode_op.out_codeblock - ) - scf.YieldOp(insert_op.out_hyper_reg) - - ops_to_insert = ( - lb_op, - ub_op, - step_op, - for_each_codeblock_op, + extract_op = qecl.ExtractCodeblockOp(hyper_reg=hyper_reg, idx=indvar) + encode_op = qecl.EncodeOp(extract_op.codeblock, init_state="zero") + insert_op = qecl.InsertCodeblockOp( + in_hyper_reg=hyper_reg, idx=indvar, codeblock=encode_op.out_codeblock ) + scf.YieldOp(insert_op.out_hyper_reg) + + ops_to_insert = ( + lb_op, + ub_op, + step_op, + for_each_codeblock_op, + ) assert ops_to_insert, "Sequence of ops to insert is empty" return ops_to_insert From 9edb7e6be624078048680b72ac8597e49af2df50 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 29 May 2026 23:54:10 -0400 Subject: [PATCH 034/120] update limitations info --- .../transforms/qecp/convert_qecp_to_quantum.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index 4bc6b06ab0..44f07c55d1 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -18,10 +18,6 @@ Known Limitations ----------------- - - * The hyper-register lowering is experimental can only target programs with more than one logical - codeblock, where there is a loop for encoding each logical codeblock. It's sufficient for the - GHZ circuit. We might have to come back to this later. * The current hyper-register lowering implementation also does not support any control flow that iterates over hyper registers, except for the encoding loop. """ From ea464477ca83855e1bb3671ac1ef672725ec61e5 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 1 Jun 2026 10:44:32 -0400 Subject: [PATCH 035/120] add zero encoding before projection --- .../transforms/qecp/convert_qecl_to_qecp.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 27ca0c2ffd..7ed5a4002b 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -832,21 +832,41 @@ def create_fabricate_subroutine(self) -> func.FuncOp: with ImplicitBuilder(block): codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n)) + #### ENCODE GROUND STATE #### + + # allocate auxiliary qubits + aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.x_tanner) + aux_qubits = [op.results[0] for op in aux_allocate_ops] + + # apply X-check gate+measurement pattern + measure_ops, zero_codeblock = self.check_pattern( + aux_qubits, codeblock, check_type=CheckType.X + ) + + # ToDo: our noise model doesn't inject noise here, but we should really be doing + # corrections here as well for a setup where noise is more ubiquitous + + # deallocate the auxiliary qubits + for meas_op in measure_ops: + qecp.DeallocAuxQubitOp(meas_op.results[1]) + + #### PROJECT TO MAGIC STATE #### + # apply H and T to the first qubit # ToDo: does it matter which one I apply it to? If so, can I deduce the correct answer from the matrix for the tanner graph? Experiment in a notebook. - initial_qubit = qecp.ExtractQubitOp(codeblock, 0) + initial_qubit = qecp.ExtractQubitOp(zero_codeblock, 0) physical_h = qecp.HadamardOp(initial_qubit) physical_t = qecp.TOp(physical_h.results[0]) - codeblock = qecp.InsertQubitOp(codeblock, 0, physical_t.results[0]) + prepped_codeblock = qecp.InsertQubitOp(zero_codeblock, 0, physical_t.results[0]) # Apply X checks + Z correction pattern - x_out_codeblock = self._qec_cycle_css_pattern(codeblock, CheckType.X, tanner_x) + x_out_codeblock = self._qec_cycle_css_pattern(prepped_codeblock, CheckType.X, tanner_x) # Apply Z checks + X correction pattern - z_out_codeblock = self._qec_cycle_css_pattern(x_out_codeblock, CheckType.Z, tanner_z) + magic_state_codeblock = self._qec_cycle_css_pattern(x_out_codeblock, CheckType.Z, tanner_z) # return the encoded codeblock - func.ReturnOp(z_out_codeblock) + func.ReturnOp(magic_state_codeblock) funcOp = func.FuncOp( name=f"fabricate_magic_state_{self.qec_code.name}", From c82d357c102d64b2ef32dacc59f39f424d9a72db Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 1 Jun 2026 16:38:00 -0400 Subject: [PATCH 036/120] rough draft of encoding routine --- .../transforms/qecp/convert_qecl_to_qecp.py | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 7ed5a4002b..f823812931 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -809,18 +809,18 @@ def create_encode_subroutine(self) -> func.FuncOp: def create_fabricate_subroutine(self) -> func.FuncOp: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a - non-fault tolerant encoding intended for use on a simulator, and not a distillation process + non-fault tolerant encoding intended for use on a simulator, and not a distillation process for generating a magic state from many noisy copies. - The encoding process involves putting the initial QEC physical qubit in the desired state, - and then using the same encoding procedure used for encoding the zero state, following the - example shown in arXiv: 0905.2794, Section VIII.A. + The encoding process involves putting the initial QEC physical qubit in the desired state + via application of a Hadamard and physical T gate, and then using the unitary encoding for + the zero state create the desired state for the codeblock. This is not the same procedure + as the syndrome-measurement based procedure for encoding the zero state; encoding via the + syndrome-measurement procedure would force the input back into the code-space, and destroy + our magic state. - The subroutine allocates auxiliary qubits for use in encoding based on the number of - rows in the X tanner graph, and deallocates them once encoding is complete. - - Note that this method does not insert the subroutine into the module op. Instead it returns - the built func.FuncOp object that can then be subsequently inserted where desired. + Note that this method does not insert the subroutine into the module op. Instead it + returns the built func.FuncOp object that can then be subsequently inserted where desired. """ codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) input_types = () @@ -832,41 +832,43 @@ def create_fabricate_subroutine(self) -> func.FuncOp: with ImplicitBuilder(block): codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n)) - #### ENCODE GROUND STATE #### - - # allocate auxiliary qubits - aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.x_tanner) - aux_qubits = [op.results[0] for op in aux_allocate_ops] - - # apply X-check gate+measurement pattern - measure_ops, zero_codeblock = self.check_pattern( - aux_qubits, codeblock, check_type=CheckType.X - ) - - # ToDo: our noise model doesn't inject noise here, but we should really be doing - # corrections here as well for a setup where noise is more ubiquitous - - # deallocate the auxiliary qubits - for meas_op in measure_ops: - qecp.DeallocAuxQubitOp(meas_op.results[1]) - - #### PROJECT TO MAGIC STATE #### + #### INITIAL WIRE TO INPUT STATE #### # apply H and T to the first qubit - # ToDo: does it matter which one I apply it to? If so, can I deduce the correct answer from the matrix for the tanner graph? Experiment in a notebook. - initial_qubit = qecp.ExtractQubitOp(zero_codeblock, 0) + # ToDo: we need to correctly select the index for pulling out the initial qubit + # hardcoding 7 for now because I believe its correct for our ordering of the Steane code, but it will (usually) be wrong for any other code + initial_qubit_index = 7 + initial_qubit = qecp.ExtractQubitOp(codeblock, initial_qubit_index) physical_h = qecp.HadamardOp(initial_qubit) physical_t = qecp.TOp(physical_h.results[0]) - prepped_codeblock = qecp.InsertQubitOp(zero_codeblock, 0, physical_t.results[0]) + prepped_codeblock = qecp.InsertQubitOp(codeblock, initial_qubit_index, physical_t.results[0]) + + #### UNITARY ZERO-ENCODING PROCEDURE #### + # ToDo: This is hardcoded to our current Steane code implementation for now, will extract to code definition so it can be generalized once its working + + extract_ops = [qecp.ExtractQubitOp(prepped_codeblock, i) for i in range(self.qec_code.n)] + aux_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} + + hadamards = (1, 2, 3) + cnot_pairs = ([1, 0], [2, 4], [6, 5], [2, 0], [3, 5], [6, 4], [2, 6], [3, 4], [1, 5], [1, 6], [3, 0]) - # Apply X checks + Z correction pattern - x_out_codeblock = self._qec_cycle_css_pattern(prepped_codeblock, CheckType.X, tanner_x) + for idx in hadamards: + h = qecp.HadamardOp(aux_qubits[idx]) + aux_qubits[idx] = h.results[0] - # Apply Z checks + X correction pattern - magic_state_codeblock = self._qec_cycle_css_pattern(x_out_codeblock, CheckType.Z, tanner_z) + for (ctrl_idx, trgt_idx) in cnot_pairs: + cnot_op = qecp.CnotOp(aux_qubits[ctrl_idx], aux_qubits[trgt_idx]) + aux_qubits[ctrl_idx] = cnot_op.results[0] + aux_qubits[trgt_idx] = cnot_op.results[1] + + # insert data qubits back into the codeblock + encoded_codeblock = prepped_codeblock + for i in range(self.qec_code.n): + insert_op = qecp.InsertQubitOp(encoded_codeblock, i, aux_qubits[i]) + encoded_codeblock = insert_op.results[0] # return the encoded codeblock - func.ReturnOp(magic_state_codeblock) + func.ReturnOp(encoded_codeblock) funcOp = func.FuncOp( name=f"fabricate_magic_state_{self.qec_code.name}", From ca7e17bddfd9722085f9a298fcff5a8f859ed322 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 10:55:18 -0400 Subject: [PATCH 037/120] fix index error --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index f823812931..68d98dc670 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -836,8 +836,8 @@ def create_fabricate_subroutine(self) -> func.FuncOp: # apply H and T to the first qubit # ToDo: we need to correctly select the index for pulling out the initial qubit - # hardcoding 7 for now because I believe its correct for our ordering of the Steane code, but it will (usually) be wrong for any other code - initial_qubit_index = 7 + # hardcoding 6 for now because I believe its correct for our ordering of the Steane code, but it will (usually) be wrong for any other code + initial_qubit_index = 6 initial_qubit = qecp.ExtractQubitOp(codeblock, initial_qubit_index) physical_h = qecp.HadamardOp(initial_qubit) physical_t = qecp.TOp(physical_h.results[0]) From 205f130c8bcf136ab5b1fdfec98eb1f543e038cc Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 12:31:32 -0400 Subject: [PATCH 038/120] update lit tests --- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 36 +++++++++++++------ .../qecl/test_xdsl_inject_noise_to_qecl.py | 9 +++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index b4c71c7e34..e83d9d0786 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -57,10 +57,15 @@ def test_alloc_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """ program = """ func.func @test_program() { - // CHECK: [[hreg10:%.+]] = qecl.alloc() : !qecl.hyperreg<1 x 1> - // CHECK: [[cb10:%.+]] = qecl.extract_block [[hreg10]][0] - // CHECK: [[cb11:%.+]] = qecl.encode[zero] [[cb10]] - // CHECK: [[hreg11:%.+]] = qecl.insert_block [[hreg10]][0], [[cb11]] +// CHECK: [[hreg10:%.+]] = qecl.alloc() : !qecl.hyperreg<1 x 1> + // CHECK: [[lb:%.+]] = arith.constant 0 : index + // CHECK: [[ub:%.+]] = arith.constant 1 : index + // CHECK: [[step:%.+]] = arith.constant 1 : index + // CHECK: [[hreg11:%.+]] = scf.for [[idx:%.+]] = [[lb]] to [[ub]] step [[step]] iter_args([[hreg1arg:%.+]] = [[hreg10]]) + // CHECK: [[cb10:%.+]] = qecl.extract_block [[hreg1arg]][[[idx]]] + // CHECK: [[cb11:%.+]] = qecl.encode[zero] [[cb10]] + // CHECK: [[hreg12:%.+]] = qecl.insert_block [[hreg1arg]][[[idx]]], [[cb11]] + // CHECK: scf.yield [[hreg12]] // CHECK-NOT: quantum.alloc %0 = quantum.alloc(1) : !quantum.reg @@ -102,11 +107,17 @@ def test_alloc_k_1_with_use(self, run_filecheck, quantum_to_qecl_pipeline_k_1): program = """ func.func @test_program() { // CHECK: [[hreg0:%.+]] = qecl.alloc() : !qecl.hyperreg<1 x 1> - // CHECK: [[cb0:%.+]] = qecl.extract_block [[hreg0]][0] - // CHECK: [[cb1:%.+]] = qecl.encode[zero] [[cb0]] - // CHECK: [[hreg1:%.+]] = qecl.insert_block [[hreg0]][0], [[cb1]] + // CHECK: [[lb:%.+]] = arith.constant 0 : index + // CHECK: [[ub:%.+]] = arith.constant 1 : index + // CHECK: [[step:%.+]] = arith.constant 1 : index + // CHECK: [[hreg1:%.+]] = scf.for [[idx:%.+]] = [[lb]] to [[ub]] step [[step]] iter_args([[hregarg:%.+]] = [[hreg0]]) + // CHECK: [[cb0:%.+]] = qecl.extract_block [[hregarg]][[[idx]]] + // CHECK: [[cb1:%.+]] = qecl.encode[zero] [[cb0]] + // CHECK: [[hreg_out:%.+]] = qecl.insert_block [[hregarg]][[[idx]]], [[cb1]] + // CHECK: scf.yield [[hreg_out]] + // CHECK: } // CHECK: [[conv_cast:%.+]] = builtin.unrealized_conversion_cast [[hreg1]] : !qecl.hyperreg<1 x 1> to !quantum.reg - // CHECK: "test.op"([[conv_cast]]) : (!quantum.reg) -> !quantum.reg + // CHECK: "test.op"([[conv_cast]]) : (!quantum.reg) -> !quantum.reg // CHECK-NOT: quantum.alloc %0 = quantum.alloc(1) : !quantum.reg %1 = "test.op"(%0) : (!quantum.reg) -> !quantum.reg @@ -865,9 +876,12 @@ def test_circuit_basic(self, run_filecheck_qjit): @qp.qnode(dev, shots=1) def circuit(): # CHECK: qecl.alloc() : !qecl.hyperreg<1 x 1> - # CHECK: qecl.extract_block {{%.+}}[0] : !qecl.hyperreg<1 x 1> -> !qecl.codeblock<1> - # CHECK: qecl.encode[zero] - # CHECK: qecl.insert_block {{%.+}}[0], {{%.+}} + # CHECK: scf.for {{.*}} { + # CHECK: qecl.extract_block + # CHECK: qecl.encode[zero] + # CHECK: qecl.insert_block + # CHECK: scf.yield + # CHECK: } # CHECK: qecl.extract_block # CHECK: qecl.qec # CHECK: qecl.hadamard {{%.+}}[0] diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_inject_noise_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_inject_noise_to_qecl.py index f263a529e9..96a57fb2f6 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_inject_noise_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_inject_noise_to_qecl.py @@ -65,9 +65,12 @@ def test_inject_noise_to_qecl_pass_integration(self, run_filecheck_qjit): @qp.qnode(dev, shots=1) def circuit(): # CHECK: qecl.alloc() : !qecl.hyperreg<1 x 1> - # CHECK: qecl.extract_block {{%.+}}[0] : !qecl.hyperreg<1 x 1> -> !qecl.codeblock<1> - # CHECK: qecl.encode[zero] - # CHECK: qecl.insert_block {{%.+}}[0], {{%.+}} + # CHECK: scf.for {{.*}} { + # CHECK: qecl.extract_block + # CHECK: qecl.encode[zero] + # CHECK: qecl.insert_block + # CHECK: scf.yield + # CHECK: } # CHECK: qecl.extract_block # CHECK: qecl.noise # CHECK: qecl.qec From 1541c7912c40dce4c7a68004e0c92f50ee1dfcfc Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 12:42:50 -0400 Subject: [PATCH 039/120] update changelog --- doc/releases/changelog-dev.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 68f025b8db..158d3b4b7d 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -64,6 +64,11 @@ folder being created and the files printed outside in the main directory. [(#2807)](https://github.com/PennyLaneAI/catalyst/pull/2807) +* Fixed a bug that passed incorrect SSA values to the final register deallocation when translating + from the `qecp` to the `quantum` dialect. This bug prevented deallocation of unneeded registers + after magic state injection. + [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) +

Internal changes ⚙️

* Removed the internal ``mlir_specs`` function which was the old backend for :func:`qp.specs`. The resource analysis pass replaces its use. @@ -136,7 +141,9 @@ - `qecp.t`, which performs a T gate on a single physical qubit. [(#2888)](https://github.com/PennyLaneAI/catalyst/pull/2888) - +* The experimental QEC pipeline now supports compilation and execution of circuits that only + include a single wire (a previously unsupported edge-case). + [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897)

Documentation 📝

From 2b97737dbe443db49417c628a76be8e3fcbee19b Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:32:32 -0400 Subject: [PATCH 040/120] Apply suggestions from code review Co-authored-by: Joey Carter --- .../transforms/qecl/test_xdsl_convert_quantum_to_qecl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index e83d9d0786..883c82a4c4 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -57,7 +57,7 @@ def test_alloc_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """ program = """ func.func @test_program() { -// CHECK: [[hreg10:%.+]] = qecl.alloc() : !qecl.hyperreg<1 x 1> + // CHECK: [[hreg10:%.+]] = qecl.alloc() : !qecl.hyperreg<1 x 1> // CHECK: [[lb:%.+]] = arith.constant 0 : index // CHECK: [[ub:%.+]] = arith.constant 1 : index // CHECK: [[step:%.+]] = arith.constant 1 : index @@ -117,7 +117,7 @@ def test_alloc_k_1_with_use(self, run_filecheck, quantum_to_qecl_pipeline_k_1): // CHECK: scf.yield [[hreg_out]] // CHECK: } // CHECK: [[conv_cast:%.+]] = builtin.unrealized_conversion_cast [[hreg1]] : !qecl.hyperreg<1 x 1> to !quantum.reg - // CHECK: "test.op"([[conv_cast]]) : (!quantum.reg) -> !quantum.reg + // CHECK: "test.op"([[conv_cast]]) : (!quantum.reg) -> !quantum.reg // CHECK-NOT: quantum.alloc %0 = quantum.alloc(1) : !quantum.reg %1 = "test.op"(%0) : (!quantum.reg) -> !quantum.reg From a7eba8d185b4bd3f6d9d3ab3eb53bb2f32ffe6ed Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 15:03:53 -0400 Subject: [PATCH 041/120] refactor unitary encoding info --- .../transforms/qecp/convert_qecl_to_qecp.py | 73 +++++++++---------- .../transforms/qecp/qec_code_lib.py | 25 +++++++ 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 68d98dc670..ce91965c68 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -375,6 +375,7 @@ def match_and_rewrite( # MARK: Apply_T Pattern + @dataclass(frozen=True) class ApplyTConversion(RewritePattern): """Converts qecl.fabricate [magic] to the equivalent subroutine of qecp gates""" @@ -407,7 +408,7 @@ def match_and_rewrite(self, funcop: func.FuncOp, rewriter: PatternRewriter): f"Circuit expressed in the qecl dialect with k={k} is not compatible with " f"lowering to a code with k={self.qec_code.k}" ) - + callee = builtin.SymbolRefAttr(self.fabricate_subroutine.sym_name) return_types = self.fabricate_subroutine.function_type.outputs.data callOp = func.CallOp(callee, arguments=(), return_types=return_types) @@ -501,7 +502,9 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: InsertBlockConversion(), ExtractBlockConversion(), EncodeOpConversion(qec_code=self.qec_code, encode_subroutine=encode_subroutine), - ApplyTConversion(qec_code=self.qec_code, fabricate_subroutine=fabricate_subroutine), + ApplyTConversion( + qec_code=self.qec_code, fabricate_subroutine=fabricate_subroutine + ), QecCycleOpConversion( qec_code=self.qec_code, qec_cycle_subroutine=qec_cycle_subroutine ), @@ -801,11 +804,9 @@ def create_encode_subroutine(self) -> func.FuncOp: ) return funcOp - # MARK: Fabricate subroutine - def create_fabricate_subroutine(self) -> func.FuncOp: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a @@ -813,58 +814,57 @@ def create_fabricate_subroutine(self) -> func.FuncOp: for generating a magic state from many noisy copies. The encoding process involves putting the initial QEC physical qubit in the desired state - via application of a Hadamard and physical T gate, and then using the unitary encoding for - the zero state create the desired state for the codeblock. This is not the same procedure - as the syndrome-measurement based procedure for encoding the zero state; encoding via the - syndrome-measurement procedure would force the input back into the code-space, and destroy + via application of a Hadamard and physical T gate, and then using the unitary encoding for + the zero state create the desired state for the codeblock. This is not the same procedure + as the syndrome-measurement based procedure for encoding the zero state; encoding via the + syndrome-measurement procedure would force the input back into the code-space, and destroy our magic state. - Note that this method does not insert the subroutine into the module op. Instead it + Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. """ + unitary_encoding_info = self.qec_code.unitary_encoding + codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) input_types = () output_types = (codeblock_type,) block = Block(arg_types=input_types) - tanner_x, tanner_z = self.insert_tanner_graph_ops_into_block(block) with ImplicitBuilder(block): - codeblock = qecp.AllocCodeblockOp(codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n)) - - #### INITIAL WIRE TO INPUT STATE #### + codeblock = qecp.AllocCodeblockOp( + codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) + ) - # apply H and T to the first qubit - # ToDo: we need to correctly select the index for pulling out the initial qubit - # hardcoding 6 for now because I believe its correct for our ordering of the Steane code, but it will (usually) be wrong for any other code - initial_qubit_index = 6 - initial_qubit = qecp.ExtractQubitOp(codeblock, initial_qubit_index) - physical_h = qecp.HadamardOp(initial_qubit) - physical_t = qecp.TOp(physical_h.results[0]) - prepped_codeblock = qecp.InsertQubitOp(codeblock, initial_qubit_index, physical_t.results[0]) + # extract qubits + extract_ops = [ + qecp.ExtractQubitOp(prepped_codeblock, i) for i in range(self.qec_code.n) + ] + magic_state_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} - #### UNITARY ZERO-ENCODING PROCEDURE #### - # ToDo: This is hardcoded to our current Steane code implementation for now, will extract to code definition so it can be generalized once its working + # apply H and T to the state prep input qubit + state_prep_index = unitary_encoding_info["state_prep_index"] + h_op = qecp.HadamardOp(magic_state_qubits[state_prep_index]) + t_op = qecp.TOp(h_op.results[0]) + magic_state_qubits[state_prep_index] = t_op.results[0] - extract_ops = [qecp.ExtractQubitOp(prepped_codeblock, i) for i in range(self.qec_code.n)] - aux_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} - - hadamards = (1, 2, 3) - cnot_pairs = ([1, 0], [2, 4], [6, 5], [2, 0], [3, 5], [6, 4], [2, 6], [3, 4], [1, 5], [1, 6], [3, 0]) + # Perform unitary encoding circuit for the code + hadamards = unitary_encoding_info["hadamard_indices"] + cnot_pairs = unitary_encoding_info["cnot_indices"] for idx in hadamards: - h = qecp.HadamardOp(aux_qubits[idx]) - aux_qubits[idx] = h.results[0] + h = qecp.HadamardOp(magic_state_qubits[idx]) + magic_state_qubits[idx] = h.results[0] - for (ctrl_idx, trgt_idx) in cnot_pairs: - cnot_op = qecp.CnotOp(aux_qubits[ctrl_idx], aux_qubits[trgt_idx]) - aux_qubits[ctrl_idx] = cnot_op.results[0] - aux_qubits[trgt_idx] = cnot_op.results[1] + for ctrl_idx, trgt_idx in cnot_pairs: + cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) + magic_state_qubits[ctrl_idx] = cnot_op.results[0] + magic_state_qubits[trgt_idx] = cnot_op.results[1] # insert data qubits back into the codeblock - encoded_codeblock = prepped_codeblock + encoded_codeblock = codeblock for i in range(self.qec_code.n): - insert_op = qecp.InsertQubitOp(encoded_codeblock, i, aux_qubits[i]) + insert_op = qecp.InsertQubitOp(encoded_codeblock, i, magic_state_qubits[i]) encoded_codeblock = insert_op.results[0] # return the encoded codeblock @@ -879,7 +879,6 @@ def create_fabricate_subroutine(self) -> func.FuncOp: return funcOp - # MARK: 1Q gate subroutines def create_transversal_1Qgate_subroutines(self) -> dict[str, func.FuncOp]: diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 72a510d787..8291341958 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -45,6 +45,23 @@ { "cnot": qecp.CnotOp, }, + { + "hadamard_indices": (1, 2, 3), + "cnot_indicies": ( + [1, 0], + [2, 4], + [6, 5], + [2, 0], + [3, 5], + [6, 4], + [2, 6], + [3, 4], + [1, 5], + [1, 6], + [3, 0], + ), + "state_prep_index": 6, + }, ), } @@ -69,6 +86,12 @@ class QecCode: op to be applied, and the indices. Assumes k=1. Does not specify indices - for now, we assume 2-qubit gates between two codeblocks, where the gate is applied between all pairs of corresponding qubits. + unitary_encoding (dict): A dictionary defining the unitary encoding for the + ground state, including indices in the code block to prepare the qubits in the |+> + state by applying a Haramard, and indices to apply CNOT gates. Also included is a + state-prep index. This is the index to apply physical gates to before encoding + to encode a non-zero state - for example, applying H-T at this index before unitary + encoding generates a magic state (not fault-tolerantly). """ name: str @@ -79,6 +102,7 @@ class QecCode: z_tanner: np.ndarray transversal_1q_gates: dict transversal_2q_gates: dict + unitary_encoding: dict def __str__(self): if self.name == "" or str.isspace(self.name): @@ -114,6 +138,7 @@ def from_dict(cls, data: dict) -> Self: ... "z_tanner": np.eye(7), ... "transversal_1q_gates": {}, ... "transversal_2q_gates": {}, + ... "unitary_encoding": {} ... }) QecCode(name='Steane', n=7, k=1, d=3) """ From 27333aa0b98d41845f0d10f474e4397c9fd7c6a6 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 15:33:59 -0400 Subject: [PATCH 042/120] fix typos --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 2 +- .../catalyst/python_interface/transforms/qecp/qec_code_lib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index ce91965c68..25ed7bf2b6 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -838,7 +838,7 @@ def create_fabricate_subroutine(self) -> func.FuncOp: # extract qubits extract_ops = [ - qecp.ExtractQubitOp(prepped_codeblock, i) for i in range(self.qec_code.n) + qecp.ExtractQubitOp(codeblock, i) for i in range(self.qec_code.n) ] magic_state_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 8291341958..85b02f538a 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -47,7 +47,7 @@ }, { "hadamard_indices": (1, 2, 3), - "cnot_indicies": ( + "cnot_indices": ( [1, 0], [2, 4], [6, 5], From 92e3fec124714fd3d3793e4abaee56896fe8be3f Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 2 Jun 2026 16:10:26 -0400 Subject: [PATCH 043/120] add integration tests --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 8728c065e5..352e30cbc9 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -545,6 +545,9 @@ def test_hyperregister_lowering(self, run_filecheck): run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) +# MARK: Integration tests + + @pytest.mark.slow class TestQECPassIntegration: """Integration lit tests for the all qec-related pass""" @@ -624,3 +627,38 @@ def ghz(): return qp.sample([m0, m1, m2]) ghz() + + @pytest.mark.parametrize("n, diagonalizing_gates, expected_res, shots", [(1, [qp.H], 0.707, 1000), (1, [qp.Z, qp.S, qp.H], 0.707, 1000), (2, [qp.H], 0, 1000), (2, [qp.Z, qp.S, qp.H], 1, 100)]) + def test_T_gate_integration(self, n, diagonalizing_gates, expected_res, shots, run_filecheck_qjit): + """Integration test for T gates.""" + + dev = qp.device("lightning.qubit", wires=1) + + @qp.qjit(capture=True, pipelines=qec_pipeline()) + @convert_qecp_to_quantum_pass + @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) + @inject_noise_to_qecl_pass + @convert_quantum_to_qecl_pass(k=1) + @qp.set_shots(shots) + @qp.qnode(dev, mcm_method="one-shot") + def circ(): + # CHECK: quantum.alloc + # CHECK: func.call @apply_T + # CHECK: fabricate_magic_state_Steane + # CHECK: qecp.assemble_tanner + # CHECK: qecp.decode_esm_css + # CHECK: quantum.custom "Hadamard" + qp.Hadamard(0) + for _ in range(n): + qp.T(0) + for op in diagonalizing_gates: + op(0) + m0 = qp.measure(0) + return qp.sample(m0) + + run_filecheck_qjit(circ) + samples = circ() + eigenvalues = [-1 if s else 1 for s in samples] + assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.05) + + From beacc03dde7ea2a6721b790428e9f262e356869c Mon Sep 17 00:00:00 2001 From: Mehrdad Malekmohammadi Date: Wed, 3 Jun 2026 03:08:23 +0000 Subject: [PATCH 044/120] skip t gates for cliffotd-only circuits --- .../qecl/convert_quantum_to_qecl.py | 13 +++++++++-- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index d164d382e8..e32fd6fd97 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -623,8 +623,17 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: module_block = op.regions[0].blocks.first assert module_block is not None, "Module has no block" - t_subroutine = self.create_t_subroutine() - module_block.add_op(t_subroutine) + + # The apply_T subroutine is built from `qecl.fabricate`. + # only emit it when the circuit actually contains a T gate. + has_t_gate = any( + isinstance(inner, quantum.CustomOp) and inner.gate_name.data == "T" + for inner in op.walk() + ) + t_subroutine = None + if has_t_gate: + t_subroutine = self.create_t_subroutine() + module_block.add_op(t_subroutine) PatternRewriteWalker( GreedyRewritePatternApplier( diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index 364a3a46b3..4ed9552afd 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -670,6 +670,29 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """ run_filecheck(program, quantum_to_qecl_pipeline_k_1) + def test_no_apply_t_subroutine_for_clifford_circuit( + self, run_filecheck, quantum_to_qecl_pipeline_k_1 + ): + """Test that the `apply_T` magic-state subroutine is NOT emitted for a Clifford-only + circuit (one with no T gates). + + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: qecl.hadamard + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = builtin.unrealized_conversion_cast %0 : !qecl.codeblock<1> to !quantum.bit + %2 = quantum.custom "Hadamard"() %1 : !quantum.bit + %3 = "test.op"(%2) : (!quantum.bit) -> !quantum.bit // To prevent DCE + return + } + // CHECK-NOT: @apply_T + // CHECK-NOT: qecl.fabricate + } + """ + run_filecheck(program, quantum_to_qecl_pipeline_k_1) + def test_gate_cnot_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): """Test that CNOT gates (`quantum.custom "CNOT"() ops) are converted to their corresponding `qecl.cnot` ops for k = 1. From 2972f90c3e5326645d3f5818ce00d7a74ee29b46 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 11:24:11 -0400 Subject: [PATCH 045/120] add lit tests --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 352e30cbc9..457771d538 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -164,6 +164,9 @@ def test_convert_type_returns_qubit_type(self, role): assert out == QubitType() +# MARK: Alloc/Dealloc aux + + class TestAuxAllocDeallocConversion: """Lowering of qecp.alloc_aux / qecp.dealloc_aux to quantum.alloc_qb / quantum.dealloc_qb.""" @@ -205,6 +208,51 @@ def test_convert_aux_operands(self, run_filecheck): run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) +# MARK: Alloc/Dealloc CB + + +class TestCodeblockAllocDeallocConversion: + """Lowering of qecp.alloc_cb/dealloc_cb to quantum.alloc/dealloc.""" + + @pytest.mark.parametrize("n", [1, 3, 7]) + def test_alloc_codeblock_lowering(self, run_filecheck, n): + """Test lowering allocation of a codeblock of n physical qubits via qecp.alloc_cb + to an allocation of a quantum register of n qubits quantum.alloc.""" + program = f""" + builtin.module {{ + // CHECK-LABEL: test_alloc_cb + func.func @test_alloc_cb() {{ + // CHECK: quantum.alloc({n}) : !quantum.reg + %0 = qecp.alloc_cb : !qecp.codeblock<1 x {n}> + // CHECK-NOT: qecp.alloc_cb + // CHECK-NOT: !qecp.codeblock + return + }} + }} + """ + run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) + + def test_dealloc_codeblock_lowering(self, run_filecheck): + """Test that qecp.dealloc_cb lowers to quantum.dealloc on the register/codeblock""" + program = """ + builtin.module { + // CHECK-LABEL: test_dealloc_cb + func.func @test_dealloc_cb() { + // CHECK: [[reg:%.+]] = "test.op"() : () -> !quantum.reg + %0 = "test.op"() : () -> !qecp.codeblock<1 x 3> + // CHECK: quantum.dealloc [[reg]] : !quantum.reg + qecp.dealloc_cb %0 : !qecp.codeblock<1 x 3> + // CHECK-NOT: qecp.dealloc_cb + return + } + } + """ + run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) + + +# MARK: Insert/extract qb + + class TestExtractInsertQubitConversion: """Lowering of qecp.extract / qecp.insert to quantum.extract / quantum.insert.""" @@ -269,6 +317,9 @@ def test_insert_lowering(self, run_filecheck): run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) +# MARK: Gates + + class TestGateMeasureConversion: """Lowering of gate and measurement operations in qecp to quantum.""" @@ -370,6 +421,25 @@ def test_s_lowering(self, run_filecheck): """ run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) + def test_t_lowering(self, run_filecheck): + """qecp.t lowers to quantum.custom "T".""" + program = """ + builtin.module { + // CHECK-LABEL: test_t + func.func @test_t() { + %cb = "test.op"() : () -> !qecp.codeblock<1 x 1> + %q0 = qecp.extract %cb[0] : !qecp.codeblock<1 x 1> -> !qecp.qubit + // CHECK: [[q1:%.+]] = quantum.custom "T"() [[q0:%.+]] : !quantum.bit + %q1 = qecp.t %q0 : !qecp.qubit + // CHECK: [[q2:%.+]] = quantum.custom "T"() [[q1:%.+]] adj : !quantum.bit + %q2 = qecp.t %q1 adj : !qecp.qubit + // CHECK-NOT: qecp.t + return %q2 : !qecp.qubit + } + } + """ + run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) + def test_cnot_lowering(self, run_filecheck): """qecp.cnot lowers to quantum.custom "CNOT".""" program = """ @@ -428,6 +498,7 @@ def test_measure_lowering(self, run_filecheck): """ run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) +# MARK: Subroutines class TestSubroutineConversion: """Lowering of subroutine funcOp and call ops with qecp types to quantum types.""" @@ -491,6 +562,8 @@ def test_subroutine_qecp_qubit_conversion(self, run_filecheck): run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) +# MARK: Hyperregisters + class TestHyperRegisterLowering: """Unit test for hyperreg related type and operations lowering.""" @@ -634,7 +707,7 @@ def test_T_gate_integration(self, n, diagonalizing_gates, expected_res, shots, r dev = qp.device("lightning.qubit", wires=1) - @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=6) @convert_qecp_to_quantum_pass @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass From f8bc160dcab1ca612afda1a2337738b21e8f37af Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 11:25:54 -0400 Subject: [PATCH 046/120] add missing MARK --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 457771d538..039f45a621 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -51,6 +51,9 @@ def test_compiler_transform_wrapper(self): assert callable(convert_qecp_to_quantum_pass) +# MARK: Type conversion + + class TestPhysicalCodeblockTypeConversion: """Type conversion from !qecp.codeblock to !quantum.reg.""" From 858adf65be4122b412d133257d766ee4a3d042f9 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 11:48:36 -0400 Subject: [PATCH 047/120] black formatting --- .../transforms/qecp/convert_qecl_to_qecp.py | 4 +--- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 25ed7bf2b6..8759398d5b 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -837,9 +837,7 @@ def create_fabricate_subroutine(self) -> func.FuncOp: ) # extract qubits - extract_ops = [ - qecp.ExtractQubitOp(codeblock, i) for i in range(self.qec_code.n) - ] + extract_ops = [qecp.ExtractQubitOp(codeblock, i) for i in range(self.qec_code.n)] magic_state_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} # apply H and T to the state prep input qubit diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 119cbeda92..5a8dac0d3b 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1017,15 +1017,15 @@ def test_nontransveral_ops_ignored(self, run_filecheck): # Mark: ApplyT + class TestLoweringApplyT: def test_apply_t_toy_code(self, run_filecheck): pass - #ToDo + # ToDo def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): - """Test that the apply_T subroutine is lowered as expected for the Steane code. - """ + """Test that the apply_T subroutine is lowered as expected for the Steane code.""" program = """ builtin.module @module_circuit { func.func @test_func() attributes {quantum.node} { @@ -1047,6 +1047,7 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): run_filecheck(program, qecl_to_qecp_steane_pipeline) raise RuntimeError() + # MARK: Integration From f10d82c96f79cc25782d403bb29fdf28cc082720 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 11:59:50 -0400 Subject: [PATCH 048/120] update changelog --- doc/releases/changelog-dev.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 45493efff2..57f5cd41f8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -161,9 +161,15 @@ - `qecp.t`, which performs a T gate on a single physical qubit. [(#2888)](https://github.com/PennyLaneAI/catalyst/pull/2888) +* The experimental `convert-qecl-to-qecp` pass has been extended to support lowering + `qecl.fabricate [magic]` to a subroutine that prepares a magic state through a simple, + non-fault tolerant encoding. + [(#2894)](https://github.com/PennyLaneAI/catalyst/pull/2894) + * The experimental QEC pipeline now supports compilation and execution of circuits that only include a single wire (a previously unsupported edge-case). [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) + * More conservative casting to tracer arrays in conditionals to preserve constant (static) values better. This can be useful for optimizations that depend on values being static. [(#2892)](https://github.com/PennyLaneAI/catalyst/pull/2892) From 2580aacfb65d3a86ef66a8cbdef42ea09018b20e Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 12:06:51 -0400 Subject: [PATCH 049/120] add changelog --- doc/releases/changelog-dev.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 57f5cd41f8..68ec54cce6 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -166,6 +166,13 @@ non-fault tolerant encoding. [(#2894)](https://github.com/PennyLaneAI/catalyst/pull/2894) +* The experimental `convert-qecp-to-quantum` pass has been extended to support translating + the following ops to the `quantum` dialect for execution and validation on simulators. + - `qecp.allocate_cb` to register allocation in the quantum dialect + - `qecp.deallocate_cb` to register deallocation in the quantum dialect + - `qecp.t` to `quantum.custom "T"` + [(#2895)](https://github.com/PennyLaneAI/catalyst/pull/2895) + * The experimental QEC pipeline now supports compilation and execution of circuits that only include a single wire (a previously unsupported edge-case). [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) From 90bec51172a78bbb8f9f859a4ec631d784186ef6 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 13:44:24 -0400 Subject: [PATCH 050/120] separate FabricateOp lowering --- .../transforms/qecp/convert_qecl_to_qecp.py | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 8759398d5b..6348d6d06a 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -213,6 +213,38 @@ def match_and_rewrite(self, op: qecl.EncodeOp, rewriter: PatternRewriter): rewriter.replace_op(op, callOp) +# MARK: Fabricate Op Pattern + + +@dataclass +class FabricateOpConversion(RewritePattern): + """Converts qecl.fabricate to the equivalent subroutine of qecp gates""" + + qec_code: QecCode + fabricate_subroutine: func.FuncOp + + @op_type_rewrite_pattern + def match_and_rewrite(self, op: qecl.FabricateOp, rewriter: PatternRewriter): + """Rewrite pattern for `qecl.fabricate [magic]` op""" + + if not op.init_state.data == "magic": + raise NotImplementedError( + "Lowering qecl.FabricateOp to the qecp dialect is only implemented " + "for init_state 'magic'" + ) + + if (k := op.out_codeblock.type.k.value.data) != self.qec_code.k: + raise CompileError( + f"Circuit expressed in the qecl dialect with k={k} is not compatible with " + f"lowering to a code with k={self.qec_code.k}" + ) + + callee = builtin.SymbolRefAttr(self.fabricate_subroutine.sym_name) + return_types = self.fabricate_subroutine.function_type.outputs.data + callOp = func.CallOp(callee, arguments=(), return_types=return_types) + rewriter.replace_op(op, callOp) + + # MARK: QEC Cycle Op Pattern @@ -373,46 +405,16 @@ def match_and_rewrite( rewriter.replace_op(op, subroutine_call_op) -# MARK: Apply_T Pattern +# MARK: Update Function Signatures @dataclass(frozen=True) -class ApplyTConversion(RewritePattern): - """Converts qecl.fabricate [magic] to the equivalent subroutine of qecp gates""" - - qec_code: QecCode - fabricate_subroutine: func.FuncOp +class UpdateFunctionSignatures(RewritePattern): + """Update types for any FuncOps in the IR whose input/output types have changed.""" @op_type_rewrite_pattern def match_and_rewrite(self, funcop: func.FuncOp, rewriter: PatternRewriter): - """Rewrite pattern for the `apply_T` subroutine""" - - if not funcop.sym_name.data == "apply_T": - return - - for op in funcop.body.walk(): - if isinstance(op, qecl.FabricateOp): - - if not op.init_state.data == "magic": - raise NotImplementedError( - "Lowering qecl.FabricateOp to the qecp dialect is only implemented " - "for init_state 'magic'" - ) - - out_codeblock = cast( - qecl.LogicalCodeBlockSSAValue | qecp.PhysicalCodeBlockSSAValue, op.out_codeblock - ) - - if (k := out_codeblock.type.k.value.data) != self.qec_code.k: - raise CompileError( - f"Circuit expressed in the qecl dialect with k={k} is not compatible with " - f"lowering to a code with k={self.qec_code.k}" - ) - - callee = builtin.SymbolRefAttr(self.fabricate_subroutine.sym_name) - return_types = self.fabricate_subroutine.function_type.outputs.data - callOp = func.CallOp(callee, arguments=(), return_types=return_types) - rewriter.replace_op(op, callOp) + """Match all FuncOps and call their update_function_type method""" funcop.update_function_type() @@ -502,7 +504,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: InsertBlockConversion(), ExtractBlockConversion(), EncodeOpConversion(qec_code=self.qec_code, encode_subroutine=encode_subroutine), - ApplyTConversion( + FabricateOpConversion( qec_code=self.qec_code, fabricate_subroutine=fabricate_subroutine ), QecCycleOpConversion( @@ -517,6 +519,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: qec_code=self.qec_code, gate_subroutines=transversal_gate_subroutines, ), + UpdateFunctionSignatures(), ] ) ).rewrite_module(op) From 23262214cdf86d8d308108d8c25e4b51e4414129 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 13:46:34 -0400 Subject: [PATCH 051/120] remove accidental changelog addition --- doc/releases/changelog-dev.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 68ec54cce6..57f5cd41f8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -166,13 +166,6 @@ non-fault tolerant encoding. [(#2894)](https://github.com/PennyLaneAI/catalyst/pull/2894) -* The experimental `convert-qecp-to-quantum` pass has been extended to support translating - the following ops to the `quantum` dialect for execution and validation on simulators. - - `qecp.allocate_cb` to register allocation in the quantum dialect - - `qecp.deallocate_cb` to register deallocation in the quantum dialect - - `qecp.t` to `quantum.custom "T"` - [(#2895)](https://github.com/PennyLaneAI/catalyst/pull/2895) - * The experimental QEC pipeline now supports compilation and execution of circuits that only include a single wire (a previously unsupported edge-case). [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) From 04f037f220804ad5aa822d122a052723ebeb49ab Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:40:23 -0400 Subject: [PATCH 052/120] Apply suggestion from @lillian542 --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6faec6f3e6..71c815abcb 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -97,6 +97,7 @@ from the `qecp` to the `quantum` dialect. This bug prevented deallocation of unneeded registers after magic state injection. [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) + * Fixed incorrect ``depth`` in :func:`~.passes.ppm_specs` when a ``quantum.extract`` appeared after a PBC op but read from a register not updated by that op. Layer grouping now checks data dependencies through insert to extract chains instead of textual op ordering. From 9f8f2556e5b567b6ea39116b8347252a93318775 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:41:03 -0400 Subject: [PATCH 053/120] Apply suggestion from @lillian542 --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 71c815abcb..8762a922ac 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -190,6 +190,7 @@ * The experimental QEC pipeline now supports compilation and execution of circuits that only include a single wire (a previously unsupported edge-case). [(#2897)](https://github.com/PennyLaneAI/catalyst/pull/2897) + * More conservative casting to tracer arrays in conditionals to preserve constant (static) values better. This can be useful for optimizations that depend on values being static. [(#2892)](https://github.com/PennyLaneAI/catalyst/pull/2892) From 59f7b0b558a6b91dddf4f269ad1b1a688a608906 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 17:42:02 -0400 Subject: [PATCH 054/120] update QEC lib tests with unitary_encoding kwarg --- .../transforms/qecp/test_qec_code_lib.py | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py index 8f95d281dd..96ec9bdc5c 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py @@ -42,6 +42,11 @@ def test_constructor(self, name: str, n: int, k: int, d: int): np.array([1] * n), transversal_1q_gates={"x": (qecp.PauliXOp, [0, 1, 2])}, transversal_2q_gates={"cnot": qecp.CnotOp}, + unitary_encoding={ + "state_prep_index": 2, + "hadamard_indices": [0, 1], + "cnot_indices": ([0, 1], [1, 2]), + }, ) assert qec_code.name == name @@ -52,18 +57,24 @@ def test_constructor(self, name: str, n: int, k: int, d: int): assert np.all(qec_code.z_tanner == np.array([1] * n)) assert qec_code.transversal_1q_gates == {"x": (qecp.PauliXOp, [0, 1, 2])} assert qec_code.transversal_2q_gates == {"cnot": qecp.CnotOp} + assert qec_code.unitary_encoding == { + "state_prep_index": 2, + "hadamard_indices": [0, 1], + "cnot_indices": ([0, 1], [1, 2]), + } @pytest.mark.parametrize( "inputs, expected_str", [ - (("Steane", 7, 1, 3, np.eye(7), np.eye(7), {}, {}), "[[7, 1, 3]] Steane"), - (("", 7, 1, 3, np.eye(7), np.eye(7), {}, {}), "[[7, 1, 3]] "), - ((" ", 7, 1, 3, np.eye(7), np.eye(7), {}, {}), "[[7, 1, 3]] "), + (("Steane", 7, 1, 3), "[[7, 1, 3]] Steane"), + (("", 7, 1, 3), "[[7, 1, 3]] "), + ((" ", 7, 1, 3), "[[7, 1, 3]] "), ], ) def test_str_representation(self, inputs, expected_str): """Test the string representation of the `QecCode` class for various inputs.""" - qec_code = QecCode(*inputs) + _, n, _, _ = inputs + qec_code = QecCode(*inputs, np.eye(n), np.eye(n), {}, {}, {}) assert str(qec_code) == expected_str @pytest.mark.parametrize( @@ -78,6 +89,7 @@ def test_str_representation(self, inputs, expected_str): "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1]]), "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, "transversal_2q_gates": {}, + "unitary_encoding": {}, }, { "name": "Shor", @@ -91,6 +103,7 @@ def test_str_representation(self, inputs, expected_str): "hadamdar": (qecp.HadamardOp, [2, 4]), }, "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "unitary_encoding": {}, }, { "name": "Unknown", @@ -102,6 +115,11 @@ def test_str_representation(self, inputs, expected_str): "extra-field": 42, "transversal_1q_gates": {"z": (qecp.PauliZOp, [4, 5, 6])}, "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "unitary_encoding": { + "state_prep_index": 2, + "hadamard_indices": [0, 1], + "cnot_indices": ([0, 1], [1, 2]), + }, }, ], ) @@ -130,6 +148,7 @@ def test_from_dict(self, data: dict): "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1]]), "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "unitary_encoding": {}, }, { "name": "Shor", @@ -140,6 +159,11 @@ def test_from_dict(self, data: dict): "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1, 0, 1]]), "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, "transversal_2q_gates": {}, + "unitary_encoding": { + "state_prep_index": 2, + "hadamard_indices": [0, 1], + "cnot_indices": ([0, 1], [1, 2]), + }, }, ], ) @@ -168,7 +192,15 @@ def test_correctable_errors_property(self, d: int, expected_t: int): correctable errors of the code, t = floor((d - 1) / 2). """ qec_code = QecCode( - "", 1, 1, d, np.array([]), np.array([]), {}, {} + "", + 1, + 1, + d, + np.array([]), + np.array([]), + {}, + {}, + {}, ) # only value of d matters assert qec_code.correctable_errors == expected_t From 6fc92d36f6930dab1282bc0225861c55fddd3388 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 17:43:25 -0400 Subject: [PATCH 055/120] update qecl to qecp tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 200 +++++++++++++----- 1 file changed, 148 insertions(+), 52 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 5a8dac0d3b..afcd98a35e 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -50,24 +50,55 @@ def fixture_qecl_to_qecp_steane_pipeline(): def fixture_get_generic_qec_code(): """Fixture factory that returns a function to create `QecCode` objects for generic QEC codes.""" - def _make_qec_code(n: int, k: int, d: int, name: str = "ToyCode", n_aux: int = 3) -> QecCode: + def _make_qec_code( + n: int, + k: int, + d: int, + name: str = "TestCode", + n_aux: int = 3, + x_tanner=None, + z_tanner=None, + transversal_1q_gates=None, + transversal_2q_gates=None, + unitary_encoding=None, + ) -> QecCode: rng = np.random.default_rng(seed=42) - return QecCode( - name=name, - n=n, - k=k, - d=d, - x_tanner=rng.integers(low=0, high=1, size=(n_aux, n)), - z_tanner=rng.integers(low=0, high=1, size=(n_aux, n)), - transversal_1q_gates={ + if x_tanner is None: + x_tanner = rng.integers(low=0, high=1, size=(n_aux, n)) + + if z_tanner is None: + z_tanner = rng.integers(low=0, high=1, size=(n_aux, n)) + + if transversal_1q_gates is None: + transversal_1q_gates = { "x": (qecp.PauliXOp, list(range(n))), "y": (qecp.PauliXOp, list(range(n))), "z": (qecp.PauliXOp, list(range(n))), "hadamard": (qecp.HadamardOp, list(range(n))), "s": (partial(qecp.SOp, adjoint=True), list(range(n))), - }, - transversal_2q_gates={"cnot": qecp.CnotOp}, + } + + if transversal_2q_gates is None: + transversal_2q_gates = {"cnot": qecp.CnotOp} + + if unitary_encoding is None: + unitary_encoding = { + "state_prep_index": rng.integers(n), + "hadamard_indices": [i for i in range(n) if i % 2], + "cnot_indices": [[i, i + 1] for i in range(n - 1)], + } + + return QecCode( + name=name, + n=n, + k=k, + d=d, + x_tanner=x_tanner, + z_tanner=z_tanner, + transversal_1q_gates=transversal_1q_gates, + transversal_2q_gates=transversal_2q_gates, + unitary_encoding=unitary_encoding, ) return _make_qec_code @@ -293,21 +324,19 @@ class TestLoweringEncode: @pytest.mark.parametrize( "k", [1, pytest.param(2, marks=pytest.mark.xfail(reason="Only k = 1 is supported"))] ) - def test_with_fake_code(self, code_name, k, run_filecheck): + def test_with_fake_code(self, code_name, k, run_filecheck, get_generic_qec_code): """Test that a single qecl.encode operation is lowered to a call to the encoding subroutine using a generic 'code' that relies on two data qubits (set by n) and two auxiliary qubits (set by the number of rows in the z_tanner graph)""" n = 2 - qec_code = QecCode( - code_name, + qec_code = get_generic_qec_code( n=n, k=k, d=1, + name=code_name, x_tanner=np.eye(n), z_tanner=np.eye(n), - transversal_1q_gates={"z": (qecp.PauliZOp, [0])}, - transversal_2q_gates={}, ) program = f""" @@ -632,7 +661,7 @@ def test_measure_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): """ run_filecheck(program, qecl_to_qecp_steane_pipeline) - def test_measure_toy_code(self, run_filecheck): + def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): """Test the lowering pattern for `qecl.measure` ops with a toy QEC code. Note that the transversal-measurement subroutine is essentially the same as the Steane code @@ -674,15 +703,11 @@ def test_measure_toy_code(self, run_filecheck): // CHECK: } } """ - qec_code = QecCode( - "TestCode", + qec_code = get_generic_qec_code( n=5, k=1, d=3, - x_tanner=np.eye(5), - z_tanner=np.eye(5), transversal_1q_gates={"z": (qecp.PauliZOp, [0, 2])}, - transversal_2q_gates={}, ) pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) @@ -695,7 +720,9 @@ def test_measure_toy_code(self, run_filecheck): [("z", (qecp.PauliZOp, []))], ], ) - def test_measure_with_missing_pauli_z_def_raise(self, run_filecheck, gate_data): + def test_measure_with_missing_pauli_z_def_raise( + self, run_filecheck, gate_data, get_generic_qec_code + ): """Test that running the convert-qecl-to-qecp pass without specifying a logical Z observable raise an error when creating the physical-measurement decoding subroutine. """ @@ -712,15 +739,11 @@ def test_measure_with_missing_pauli_z_def_raise(self, run_filecheck, gate_data): } """ - qec_code = QecCode( - "TestCode", + qec_code = get_generic_qec_code( n=7, k=1, d=3, - x_tanner=np.eye(7), - z_tanner=np.eye(7), transversal_1q_gates=dict(gate_data), - transversal_2q_gates={}, ) pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) @@ -736,22 +759,18 @@ def test_measure_with_missing_pauli_z_def_raise(self, run_filecheck, gate_data): class TestLoweringTransversalGates: """Unit tests for lowering transversal gates in the convert-qecl-to-qecp pass.""" - def test_single_qubit_op_lowering_generic(self, run_filecheck): + def test_single_qubit_op_lowering_generic(self, run_filecheck, get_generic_qec_code): """Test that a generic QEC code lowers ops as instructed. In this case (n=3, x is transversal and applied on indicies 0 and 2), we expect to extract 3 qubits, apply the pattern XIX on them, and re-insert them.""" n, k = (3, 1) - qec_code = QecCode( - "TestCode", + qec_code = get_generic_qec_code( n=n, k=k, d=1, - x_tanner=np.eye(n), - z_tanner=np.eye(n), transversal_1q_gates={"x": (qecp.PauliXOp, [0, 2]), "z": (qecp.PauliZOp, [0, 2])}, - transversal_2q_gates={}, ) program = f""" @@ -781,20 +800,17 @@ def test_single_qubit_op_lowering_generic(self, run_filecheck): pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) run_filecheck(program, pipeline) - def test_two_qubit_op_lowering_generic(self, run_filecheck): + def test_two_qubit_op_lowering_generic(self, run_filecheck, get_generic_qec_code): """Test that a generic QEC code lowers ops as instructed. In this case (n=3, x is transversal and applied on indicies 0 and 2), we expect to extract 3 qubits, apply the pattern XIX on them, and re-insert them.""" n, k = (3, 1) - qec_code = QecCode( - "TestCode", + qec_code = get_generic_qec_code( n=n, k=k, d=1, - x_tanner=np.eye(n), - z_tanner=np.eye(n), transversal_1q_gates={"z": (qecp.PauliZOp, [0])}, transversal_2q_gates={"cnot": qecp.CnotOp}, ) @@ -979,20 +995,16 @@ def test_cnot_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipeline) run_filecheck(program, qecl_to_qecp_steane_pipeline) - def test_nontransveral_ops_ignored(self, run_filecheck): + def test_nontransveral_ops_ignored(self, run_filecheck, get_generic_qec_code): """Test that a generic QEC code lowers ops as instructed""" n, k = (3, 1) - qec_code = QecCode( - "TestCode", + qec_code = get_generic_qec_code( n=n, k=k, d=1, - x_tanner=np.eye(n), - z_tanner=np.eye(n), transversal_1q_gates={"x": (qecp.PauliXOp, [0, 1]), "z": (qecp.PauliZOp, [0, 1])}, - transversal_2q_gates={}, ) program = f""" @@ -1015,17 +1027,102 @@ def test_nontransveral_ops_ignored(self, run_filecheck): run_filecheck(program, pipeline) -# Mark: ApplyT +# Mark: FabricateOp -class TestLoweringApplyT: +class TestLoweringFabricateOp: - def test_apply_t_toy_code(self, run_filecheck): - pass - # ToDo + def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): + """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state + fabrication subroutine with a toy code, and that the subroutine performs H-T + state injection on the code's state_prep_index followed by the unitary encoding + as defined by `hadamard_indices` and `hadamard_indices`.""" + + qec_code = get_generic_qec_code( + n=3, + k=1, + d=1, + unitary_encoding={ + "hadamard_indices": (0, 2), + "cnot_indices": ([0, 1], [2, 0]), + "state_prep_index": 1, + }, + ) + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_TestCode() : () -> !qecp.codeblock<1 x 3> + %0 = qecl.fabricate[magic] : !qecl.codeblock<1> + return + } + // CHECK-LABEL: func.func private @fabricate_magic_state_TestCode() -> !qecp.codeblock<1 x 3> { + // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 3> + // Extract qubits + // CHECK: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 3> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 3> -> !qecp.qubit + // CHECK: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 3> -> !qecp.qubit + // State injection on the state_prep_index (q1) + // CHECK: [[q1_1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit + // CHECK: [[q1_2:%.+]] = qecp.t [[q1_1]] : !qecp.qubit + // Unitary encoding + // CHECK: [[q0_1:%.+]] = qecp.hadamard [[q0]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit + // CHECK: [[q0_2:%.+]], [[q1_out:%.+]] = qecp.cnot [[q0_1]], [[q1_2]] : !qecp.qubit, !qecp.qubit + // CHECK: [[q2_out:%.+]], [[q0_out:%.+]] = qecp.cnot [[q2_1]], [[q0_2]] : !qecp.qubit, !qecp.qubit + // Insert qubits and return + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb]][0], [[q0_out]] + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_out]] + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_out]] + // CHECK: func.return [[cb_3:%.+]] : !qecp.codeblock<1 x 3> + // CHECK: } + } + """ + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) + run_filecheck(program, pipeline) + + def test_fabricate_lowering_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): + """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state + fabrication subroutine when using the Steane code, and that the subroutine performs + H-T state injection on the Steane code's state_prep_index (qubit 6) followed by the + unitary encoding.""" + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_Steane() : () -> !qecp.codeblock<1 x 7> + %0 = qecl.fabricate[magic] : !qecl.codeblock<1> + return + } + // CHECK-LABEL: func.func private @fabricate_magic_state_Steane() -> !qecp.codeblock<1 x 7> { + // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 7> + // CHECK: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // State injection on the state_prep_index (qubit 6): H then T + // CHECK: [[h_inj:%.+]] = qecp.hadamard [[q6]] : !qecp.qubit + // CHECK: [[t_inj:%.+]] = qecp.t [[h_inj]] : !qecp.qubit + // Unitary encoding: Hadamards on indices 1, 2, 3 + // CHECK: [[h1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit + // CHECK: [[h2:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit + // CHECK: [[h3:%.+]] = qecp.hadamard [[q3]] : !qecp.qubit + // First few CNOTs of the encoding circuit + // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[h1]], [[q0]] : !qecp.qubit, !qecp.qubit + // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[h2]], [[q4]] : !qecp.qubit, !qecp.qubit + // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[t_inj]], [[q5]] : !qecp.qubit, !qecp.qubit + // CHECK: func.return {{%.+}} : !qecp.codeblock<1 x 7> + // CHECK: } + } + """ + run_filecheck(program, qecl_to_qecp_steane_pipeline) def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): - """Test that the apply_T subroutine is lowered as expected for the Steane code.""" + """Test that the call signature for the apply_T subroutine is updated as expected.""" + program = """ builtin.module @module_circuit { func.func @test_func() attributes {quantum.node} { @@ -1045,7 +1142,6 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) - raise RuntimeError() # MARK: Integration From bd9b87d0b3a92eac5207bf31a8b8d7c91a134dcb Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 17:44:00 -0400 Subject: [PATCH 056/120] add check + documentation for clarity --- .../transforms/qecp/convert_qecl_to_qecp.py | 8 ++++++++ .../python_interface/transforms/qecp/qec_code_lib.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 6348d6d06a..46f400f8fc 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -828,6 +828,14 @@ def create_fabricate_subroutine(self) -> func.FuncOp: """ unitary_encoding_info = self.qec_code.unitary_encoding + required_keys = {"state_prep_index", "hadamard_indices", "cnot_indices"} + if not required_keys.issubset(unitary_encoding_info): + raise CompileError( + f"QEC code '{self.qec_code.name}' does not define a unitary encoding " + f"(missing {required_keys - set(unitary_encoding_info)}); cannot lower " + f"qecl.fabricate [magic] for this code." + ) + codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) input_types = () output_types = (codeblock_type,) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 85b02f538a..e7a3109a83 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -60,6 +60,10 @@ [1, 6], [3, 0], ), + # The state_prep_index is the index of the physical qubit that the state is + # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). + # Must be consistent with the qubit treated as the encoding "input" by the + # cnot_indices ordering above. "state_prep_index": 6, }, ), From 5623f21116b032e612bdee58f951bc8e24385113 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 17:47:27 -0400 Subject: [PATCH 057/120] revert premature removal of symbol-dce --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index aa84533551..3ad74e12de 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -556,6 +556,7 @@ def test_qec_pass_ghz_lightning_integration(self, run_filecheck_qjit): @qp.qjit(capture=True, pipelines=qec_pipeline()) @convert_qecp_to_quantum_pass + @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass @qp.transform(pass_name="symbol-dce") @@ -610,6 +611,7 @@ def test_qec_pass_ghz_nullqubit_integration(self): @qp.qjit(capture=True, pipelines=qec_pipeline()) @convert_qecp_to_quantum_pass + @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass @qp.transform(pass_name="symbol-dce") From e697ac96e09ea2ff46c4f80c1a1091d8f63cf3f0 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 18:30:52 -0400 Subject: [PATCH 058/120] draft test --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 2709451696..d7b37a9ffa 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -922,6 +922,50 @@ def test_single_qubit_gate_lowering_Steane( run_filecheck(program, qecl_to_qecp_steane_pipeline) + def test_adjoint_s_lowering_Steane( + self, run_filecheck, qecl_to_qecp_steane_pipeline + ): + """Test that using the Steane code lowers Hadamard, Identity and S ops as expected. These ops + are applied on all qubits in the codeblock. For the S operator, the adjoint is applied.""" + + program = f""" + builtin.module @module_circuit {{ + func.func @test_func() attributes {{quantum.node}} {{ + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 7> + // CHECK-NEXT: [[codeblock2:%.+]] = func.call @s_adj_Steane([[codeblock]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> + // CHECK-NOT: qecl.s + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.s %0[0] adj : !qecl.codeblock<1> + return + }} + // CHECK: func.func private @s_adj_Steane([[codeblock_in:%.+]]: !qecp.codeblock<1 x 7>) + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.s [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.s [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.s [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.s [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.s [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.s [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.s [[q6]] : !qecp.qubit + // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit + // CHECK-NEXT: func.return [[codeblock_out]] + }} + """ + + run_filecheck(program, qecl_to_qecp_steane_pipeline) + def test_cnot_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): """Test that using the Steane code lowers ops as expected""" From ff8be88f8ad487efd1102484a344dab2d03efd1d Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 3 Jun 2026 19:23:26 -0400 Subject: [PATCH 059/120] main + bugfix combined better --- .../transforms/qecp/convert_qecp_to_quantum.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index ac099c50fd..d0ad16234c 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -340,8 +340,12 @@ def _apply_experimental_hyperregister_lowering(self, op: builtin.ModuleOp): quantum_op.codeblock.replace_all_uses_with(regs[idx]) case qecp.InsertCodeblockOp(): qecp_ops_to_remove.append(quantum_op) - dealloc = quantum.DeallocOp(quantum_op.codeblock) - rewriter.insert_op(dealloc) + if quantum_op.idx is not None: + idx = resolve_constant_params(quantum_op.idx) + elif quantum_op.idx_attr is not None: + idx = quantum_op.idx_attr.value.data + dealloc_op = quantum.DeallocOp(quantum_op.codeblock) + dealloced_regs[regs[idx]] = dealloc_op quantum_op.results[0].replace_all_uses_with(qecp_alloc_op.results[0]) case qecp.DeallocOp(): rewriter.erase_op(quantum_op) From 1183520498fd13575ac10049a1fc1cae3d469a6e Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 11:16:05 -0400 Subject: [PATCH 060/120] improvement --- .../transforms/qecp/convert_qecp_to_quantum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index d0ad16234c..e0c8432fbc 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -344,8 +344,7 @@ def _apply_experimental_hyperregister_lowering(self, op: builtin.ModuleOp): idx = resolve_constant_params(quantum_op.idx) elif quantum_op.idx_attr is not None: idx = quantum_op.idx_attr.value.data - dealloc_op = quantum.DeallocOp(quantum_op.codeblock) - dealloced_regs[regs[idx]] = dealloc_op + dealloced_regs[idx] = quantum.DeallocOp(quantum_op.codeblock) quantum_op.results[0].replace_all_uses_with(qecp_alloc_op.results[0]) case qecp.DeallocOp(): rewriter.erase_op(quantum_op) From 52354c3a882c2cd97895ea7f17e77dec5304abe8 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 11:30:51 -0400 Subject: [PATCH 061/120] update test after swapped corrections --- .../transforms/qecl/test_xdsl_convert_quantum_to_qecl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index 5c80f107d4..561e33ba1e 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -671,8 +671,8 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): // CHECK-NEXT: [[mres:%.+]], [[in_codeblock3:%.+]] = qecl.measure [[in_codeblock2]][0] // CHECK-NEXT: qecl.dealloc_cb [[in_codeblock3]] // CHECK-NEXT: [[out_codeblock:%.+]] = scf.if [[mres]] -> (!qecl.codeblock<1>) - // CHECK-NEXT: [[s_corrected_cb:%.+]] = qecl.s [[magic_cb2]][0] - // CHECK-NEXT: [[corrected_cb:%.+]] = qecl.x [[s_corrected_cb]] + // CHECK-NEXT: [[x_corrected_cb:%.+]] = qecl.x [[magic_cb2]][0] + // CHECK-NEXT: [[corrected_cb:%.+]] = qecl.s [[x_corrected_cb]] // CHECK-NEXT: scf.yield [[corrected_cb]] // CHECK-NEXT: else // CHECK-NEXT: scf.yield [[magic_cb2]] From 976f079fac92cfe79ca559cf43e8686700f15127 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 13:00:09 -0400 Subject: [PATCH 062/120] merge --- .../qecp/convert_qecp_to_quantum.py | 2 ++ .../qecp/test_xdsl_convert_qecp_to_quantum.py | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index d29277c8b2..5e012a03d9 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -142,6 +142,7 @@ def match_and_rewrite(self, op: qecp.DeallocAuxQubitOp, rewriter: PatternRewrite """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary qubit.""" rewriter.replace_op(op, quantum.DeallocQubitOp(op.qubit)) + @dataclass(frozen=True) class AllocCodeblockConversion(RewritePattern): """Op conversion pattern from qecp.alloc_cb to quantum.alloc.""" @@ -161,6 +162,7 @@ def match_and_rewrite(self, op: qecp.DeallocCodeblockOp, rewriter: PatternRewrit """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary qubit.""" rewriter.replace_op(op, quantum.DeallocOp(op.codeblock)) + # MARK: Data qubit extract and insertion patterns diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 1ee17c2cdd..80713408e1 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -219,7 +219,7 @@ class TestCodeblockAllocDeallocConversion: @pytest.mark.parametrize("n", [1, 3, 7]) def test_alloc_codeblock_lowering(self, run_filecheck, n): - """Test lowering allocation of a codeblock of n physical qubits via qecp.alloc_cb + """Test lowering allocation of a codeblock of n physical qubits via qecp.alloc_cb to an allocation of a quantum register of n qubits quantum.alloc.""" program = f""" builtin.module {{ @@ -501,8 +501,10 @@ def test_measure_lowering(self, run_filecheck): """ run_filecheck(program, (ConvertQecPhysicalToQuantumPass(),)) + # MARK: Subroutines + class TestSubroutineConversion: """Lowering of subroutine funcOp and call ops with qecp types to quantum types.""" @@ -567,6 +569,7 @@ def test_subroutine_qecp_qubit_conversion(self, run_filecheck): # MARK: Hyperregisters + class TestHyperRegisterLowering: """Unit test for hyperreg related type and operations lowering.""" @@ -706,8 +709,18 @@ def ghz(): ghz() - @pytest.mark.parametrize("n, diagonalizing_gates, expected_res, shots", [(1, [qp.H], 0.707, 1000), (1, [qp.Z, qp.S, qp.H], 0.707, 1000), (2, [qp.H], 0, 1000), (2, [qp.Z, qp.S, qp.H], 1, 100)]) - def test_T_gate_integration(self, n, diagonalizing_gates, expected_res, shots, run_filecheck_qjit): + @pytest.mark.parametrize( + "n, diagonalizing_gates, expected_res, shots", + [ + (1, [qp.H], 0.707, 1000), + (1, [qp.Z, qp.S, qp.H], 0.707, 1000), + (2, [qp.H], 0, 1000), + (2, [qp.Z, qp.S, qp.H], 1, 100), + ], + ) + def test_T_gate_integration( + self, n, diagonalizing_gates, expected_res, shots, run_filecheck_qjit + ): """Integration test for T gates.""" dev = qp.device("lightning.qubit", wires=1) @@ -738,5 +751,3 @@ def circ(): samples = circ() eigenvalues = [-1 if s else 1 for s in samples] assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.05) - - From c223afc0c0685fcf4045cb5e9f2a1165b7072e47 Mon Sep 17 00:00:00 2001 From: Mehrdad Malekmohammadi Date: Thu, 4 Jun 2026 18:01:52 +0000 Subject: [PATCH 063/120] edit comment --- .../python_interface/transforms/qecl/convert_quantum_to_qecl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index e32fd6fd97..aad661b5ee 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -624,7 +624,6 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: module_block = op.regions[0].blocks.first assert module_block is not None, "Module has no block" - # The apply_T subroutine is built from `qecl.fabricate`. # only emit it when the circuit actually contains a T gate. has_t_gate = any( isinstance(inner, quantum.CustomOp) and inner.gate_name.data == "T" From 728af3939815e6bd3cbad70408fdc7eb2c997a47 Mon Sep 17 00:00:00 2001 From: Mehrdad Malekmohammadi Date: Thu, 4 Jun 2026 18:13:52 +0000 Subject: [PATCH 064/120] add changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 16382ea7e2..57368fe0e2 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -133,6 +133,7 @@ * The experimental compiler pass `convert-quantum-to-qecl` has been extended to lower `quantum.custom "T"` gates to the `qecl` layer as a subroutine using a magic state. [(#2870)](https://github.com/PennyLaneAI/catalyst/pull/2870) + [(#2917)](https://github.com/PennyLaneAI/catalyst/pull/2917) * The reference semantics Pauli Product Measurement operation `pbc.ref.ppm` was added. [(#2773)](https://github.com/PennyLaneAI/catalyst/pull/2773) From 6aead5fc3e076e93a3f9084e1cf058731d2fb805 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 14:24:57 -0400 Subject: [PATCH 065/120] black formatting + draft test --- .../transforms/qecp/convert_qecl_to_qecp.py | 3 +++ .../transforms/qecp/qec_code_lib.py | 1 + .../qecp/test_xdsl_convert_qecl_to_qecp.py | 4 +-- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 27 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index d9843790cb..f83a921f5c 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -327,6 +327,9 @@ def match_and_rewrite( gate_name = op.name.split(".")[1] # op.name is "qecl.gate_name" + if getattr(op, "adjoint", None): + gate_name = f"{gate_name}_adj" + op_codeblocks = ( (op.in_ctrl_codeblock, op.in_trgt_codeblock) if gate_name in self.qec_code.transversal_2q_gates diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 72a510d787..562f8681f6 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -41,6 +41,7 @@ "z": (qecp.PauliZOp, [4, 5, 6]), "hadamard": (qecp.HadamardOp, [0, 1, 2, 3, 4, 5, 6]), "s": (partial(qecp.SOp, adjoint=True), [0, 1, 2, 3, 4, 5, 6]), + "s_adj": (qecp.SOp, [0, 1, 2, 3, 4, 5, 6]), }, { "cnot": qecp.CnotOp, diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index d7b37a9ffa..dd9eb84e17 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -922,9 +922,7 @@ def test_single_qubit_gate_lowering_Steane( run_filecheck(program, qecl_to_qecp_steane_pipeline) - def test_adjoint_s_lowering_Steane( - self, run_filecheck, qecl_to_qecp_steane_pipeline - ): + def test_adjoint_s_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): """Test that using the Steane code lowers Hadamard, Identity and S ops as expected. These ops are applied on all qubits in the codeblock. For the S operator, the adjoint is applied.""" diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 3d77d1fb37..17b0380cd5 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -626,3 +626,30 @@ def ghz(): return qp.sample([m0, m1, m2]) ghz() + + @pytest.mark.parametrize( + "gates, expected_results", + [([qp.H], 0), ([qp.H, qp.Y, qp.H], 0), ([qp.H, qp.X, qp.H], -1), ([qp.H, qp.S, qp.H],)], + ) + def test_qec_single_gate_ops_integration(self): + """Integration tests for combinations of single-gate ops.""" + + dev = qp.device("lightning.qubit", wires=3) + + @qp.qjit(capture=True, pipelines=qec_pipeline()) + @convert_qecp_to_quantum_pass + @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) + @inject_noise_to_qecl_pass + @qp.transform(pass_name="symbol-dce") + @convert_quantum_to_qecl_pass(k=1) + @qp.set_shots(10) + @qp.qnode(dev, mcm_method="one-shot") + def ghz(): + qp.Hadamard(0) + qp.Y + m0 = qp.measure(0) + m1 = qp.measure(1) + m2 = qp.measure(2) + return qp.sample([m0, m1, m2]) + + ghz() From d3a56b7e9ec105788b683bb20c47524d63a237ff Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:06:04 -0400 Subject: [PATCH 066/120] Apply suggestions from code review Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 3ad74e12de..88b94e5bfe 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -559,7 +559,6 @@ def test_qec_pass_ghz_lightning_integration(self, run_filecheck_qjit): @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(1) @qp.qnode(dev, mcm_method="one-shot") @@ -614,7 +613,6 @@ def test_qec_pass_ghz_nullqubit_integration(self): @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) @qp.set_shots(10) @qp.qnode(dev, mcm_method="one-shot") From 2a4a6a55bb78aa56c77cd8e4ac18bb027be6cbfd Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 15:18:41 -0400 Subject: [PATCH 067/120] update draft of test --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 17b0380cd5..381f274d3c 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -629,12 +629,31 @@ def ghz(): @pytest.mark.parametrize( "gates, expected_results", - [([qp.H], 0), ([qp.H, qp.Y, qp.H], 0), ([qp.H, qp.X, qp.H], -1), ([qp.H, qp.S, qp.H],)], + [ + # expected results are (expval(X), expval(Y) expval(Z)) + # pauli ops all behave as expected from ground state + ([qp.X], (0, 0, -1)), + ([qp.Y], (0, 0, -1)), + ([qp.Z], (0, 0, 1)), + # hadamard projects onto x-axis + ([qp.H], (1, 0, 0)), + # x after hadamard does nothing, y and z flip expval(X) + ([qp.H, qp.X], (1, 0, 0)), + ([qp.H, qp.Y], (-1, 0, 0)), + ([qp.H, qp.Z], (-1, 0, 0)), + # adjoint and S adjoint rotate onto y axis in expected directions + ([qp.H, qp.S], (0, 1, 0)), + ([qp.H, qp.adjoint(qp.S)], (0, -1, 0)), + # hadamard is self-inverse + ([qp.H, qp.H], (0, 0, 1)), + ], ) - def test_qec_single_gate_ops_integration(self): - """Integration tests for combinations of single-gate ops.""" + def test_qec_single_clifford_gate_ops_integration(self, gates, expected_results): + """Integration tests for combinations of single Clifford gate ops.""" - dev = qp.device("lightning.qubit", wires=3) + dev = qp.device("lightning.qubit", wires=1) + + # compile_pipeline = qp.CompilePipeline() @qp.qjit(capture=True, pipelines=qec_pipeline()) @convert_qecp_to_quantum_pass @@ -642,14 +661,25 @@ def test_qec_single_gate_ops_integration(self): @inject_noise_to_qecl_pass @qp.transform(pass_name="symbol-dce") @convert_quantum_to_qecl_pass(k=1) - @qp.set_shots(10) + @qp.set_shots(1) @qp.qnode(dev, mcm_method="one-shot") - def ghz(): - qp.Hadamard(0) - qp.Y + def test_circ(): + for gate in gates: + gate(0) + # gate(1) + # gate(2) + qp.H(0) + # qp.Z(1) + # qp.S(1) + # qp.H(1) m0 = qp.measure(0) - m1 = qp.measure(1) - m2 = qp.measure(2) - return qp.sample([m0, m1, m2]) + # m1 = qp.measure(1) + # m2 = qp.measure(2) + return qp.sample(m0)#, qp.sample(m1), qp.sample(m2) - ghz() + all_samples = test_circ() + print(all_samples) + raise RuntimeError() + + # res = [np.mean([-1 if s else 1 for s in sample]) for sample in all_samples] + # assert np.allclose(res, expected_result) From 3f4d7925f601abc9aaad5e2c801c26000d85e52b Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 15:36:26 -0400 Subject: [PATCH 068/120] tidying up --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 2 +- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 46f400f8fc..cc385be2b3 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -24,7 +24,7 @@ * No support for QEC codes where the number of logical qubits per codeblock, k, is greater than 1. * No support for non-CSS codes - * Only supports lowering of transversal gates and measurements. Does not support non-Clifford gates. + * Only supports the tranvsersal gates defined in the `QecCode` + T gates * Only logical Pauli Z observables (computational basis) are supported for lowering `qecl.measure` operations. * Support for control flow in the user program is not fully implemented or tested diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index a612c371ad..b013a1c47b 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -46,6 +46,7 @@ def fixture_qecl_to_qecp_steane_pipeline(): return (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Steane")),) +# pylint: disable=too-many-arguments @pytest.fixture(name="get_generic_qec_code", scope="module") def fixture_get_generic_qec_code(): """Fixture factory that returns a function to create `QecCode` objects for generic QEC codes.""" @@ -54,6 +55,7 @@ def _make_qec_code( n: int, k: int, d: int, + *, name: str = "TestCode", n_aux: int = 3, x_tanner=None, @@ -1031,6 +1033,7 @@ def test_nontransveral_ops_ignored(self, run_filecheck, get_generic_qec_code): class TestLoweringFabricateOp: + """Test lowering for the qecl.fabricate op""" def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state From 21db585ecbce7a1f5d785423f9850078962a57d8 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:41:37 -0400 Subject: [PATCH 069/120] Apply suggestions from code review Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 80713408e1..7d2f083b56 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -638,7 +638,6 @@ def test_qec_pass_ghz_lightning_integration(self, run_filecheck_qjit): @qp.qjit(capture=True, pipelines=qec_pipeline()) @convert_qecp_to_quantum_pass - @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass @convert_quantum_to_qecl_pass(k=1) @@ -692,7 +691,6 @@ def test_qec_pass_ghz_nullqubit_integration(self): @qp.qjit(capture=True, pipelines=qec_pipeline()) @convert_qecp_to_quantum_pass - @qp.transform(pass_name="symbol-dce") @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) @inject_noise_to_qecl_pass @convert_quantum_to_qecl_pass(k=1) @@ -750,4 +748,5 @@ def circ(): run_filecheck_qjit(circ) samples = circ() eigenvalues = [-1 if s else 1 for s in samples] - assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.05) + # could have a lower atol with more shots, but given test duration, not worth it + assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) From 7369f5caead11ae6461d63bcf61dfa3995c29f6a Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 16:20:11 -0400 Subject: [PATCH 070/120] fix typos --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 2 +- .../catalyst/python_interface/transforms/qecp/qec_code_lib.py | 2 +- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index cc385be2b3..3b160ba36c 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -24,7 +24,7 @@ * No support for QEC codes where the number of logical qubits per codeblock, k, is greater than 1. * No support for non-CSS codes - * Only supports the tranvsersal gates defined in the `QecCode` + T gates + * Only supports the transversal gates defined in the `QecCode` + T gates * Only logical Pauli Z observables (computational basis) are supported for lowering `qecl.measure` operations. * Support for control flow in the user program is not fully implemented or tested diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index e7a3109a83..71ef71c628 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -92,7 +92,7 @@ class QecCode: between all pairs of corresponding qubits. unitary_encoding (dict): A dictionary defining the unitary encoding for the ground state, including indices in the code block to prepare the qubits in the |+> - state by applying a Haramard, and indices to apply CNOT gates. Also included is a + state by applying a Hadamard, and indices to apply CNOT gates. Also included is a state-prep index. This is the index to apply physical gates to before encoding to encode a non-zero state - for example, applying H-T at this index before unitary encoding generates a magic state (not fault-tolerantly). diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index b013a1c47b..9c244147a6 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1039,7 +1039,7 @@ def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine with a toy code, and that the subroutine performs H-T state injection on the code's state_prep_index followed by the unitary encoding - as defined by `hadamard_indices` and `hadamard_indices`.""" + as defined by `hadamard_indices` and `cnot_indices`.""" qec_code = get_generic_qec_code( n=3, From 1d064d20f736a0d8e6bf81fc0f9f67febaa96bec Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 16:29:24 -0400 Subject: [PATCH 071/120] fix typos --- .../transforms/qecp/convert_qecp_to_quantum.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py index 7fb9140bb6..6c5f99fb9f 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecp_to_quantum.py @@ -149,17 +149,17 @@ class AllocCodeblockConversion(RewritePattern): @op_type_rewrite_pattern def match_and_rewrite(self, op: qecp.AllocCodeblockOp, rewriter: PatternRewriter): - """Op conversion rewrite pattern for lowering ops that allocate an auxiliary qubit.""" + """Op conversion rewrite pattern for lowering ops that allocate an auxiliary codeblock.""" rewriter.replace_op(op, quantum.AllocOp(op.codeblock.type.n)) @dataclass(frozen=True) class DeallocCodeblockConversion(RewritePattern): - """Op conversion pattern from qecp.dealloc_cb to quantum.alloc.""" + """Op conversion pattern from qecp.dealloc_cb to quantum.dealloc.""" @op_type_rewrite_pattern def match_and_rewrite(self, op: qecp.DeallocCodeblockOp, rewriter: PatternRewriter): - """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary qubit.""" + """Op conversion rewrite pattern for lowering ops that deallocate an auxiliary codeblock.""" rewriter.replace_op(op, quantum.DeallocOp(op.codeblock)) @@ -391,6 +391,8 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: [ AllocCodeblockConversion(), DeallocCodeblockConversion(), + # AllocCodeblock conversion must come before Type conversion, because + # it relies on accessing op.codeblock.type.n to get the register size PhysicalCodeblockTypeConversion(recursive=True), QecPhysicalQubitTypeConversion(recursive=True), AllocAuxQubitConversion(), From dd0532d39f254b34dd0969041c32828176115d2d Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 16:36:41 -0400 Subject: [PATCH 072/120] add clarifying comment --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 7d2f083b56..538faa89f1 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -713,7 +713,8 @@ def ghz(): (1, [qp.H], 0.707, 1000), (1, [qp.Z, qp.S, qp.H], 0.707, 1000), (2, [qp.H], 0, 1000), - (2, [qp.Z, qp.S, qp.H], 1, 100), + # 2 T-gates in the Y basis is always 1, we can use fewer shots + (2, [qp.Z, qp.S, qp.H], 1, 20), ], ) def test_T_gate_integration( From a4aa34d3e5e6ad15d071b5b312f8a44fdc0fcac7 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 20:33:10 -0400 Subject: [PATCH 073/120] add 'magic_conj' attribute --- frontend/catalyst/python_interface/dialects/qecl.py | 1 + mlir/include/QecLogical/IR/QecLogicalAttrDefs.td | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/catalyst/python_interface/dialects/qecl.py b/frontend/catalyst/python_interface/dialects/qecl.py index 0e64b73bee..1196cf7ddd 100644 --- a/frontend/catalyst/python_interface/dialects/qecl.py +++ b/frontend/catalyst/python_interface/dialects/qecl.py @@ -67,6 +67,7 @@ class LogicalCodeblockInitState(StrEnum): Zero = "zero" # |0⟩ Magic = "magic" # |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) + Magic_conj = "magic_conj" # |m̄⟩ = |0⟩ + e^{-iπ/4}|1⟩ (Magic state conjugated) @irdl_attr_definition diff --git a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td index d6a3596487..00ab72e624 100644 --- a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td +++ b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td @@ -28,6 +28,7 @@ def LogicalCodeblockInitState : I32EnumAttr<"LogicalCodeblockInitState", [ I32EnumAttrCase<"Zero", 0, "zero">, // |0⟩ I32EnumAttrCase<"Magic", 1, "magic"> // |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) + I32EnumAttrCase<"Magic_conj", 1, "magic_conj"> // |m̄⟩ = |0⟩ + e^{-iπ/4}|1⟩ (Magic state conjugated) ]> { let cppNamespace = "catalyst::qecl"; let genSpecializedAttr = 0; From 3dc9fcce2d8b543eea10bea35eeb3ea23632d7e3 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 20:33:51 -0400 Subject: [PATCH 074/120] add quantum to qecl lowering --- .../transforms/qecl/convert_quantum_to_qecl.py | 17 ++++++++++++----- .../qecl/test_xdsl_convert_quantum_to_qecl.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index dedd13f5db..4862602c31 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -356,6 +356,7 @@ class CustomOpConversion(RewritePattern): """ t_subroutine: func.FuncOp + t_adj_subroutine: func.FuncOp @op_type_rewrite_pattern def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): @@ -369,6 +370,7 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): ops_to_insert = self._get_qecl_ops_for_single_qubit_gate(op) case "T": + subroutine = self.t_adj_subroutine if op.adjoint else self.t_subroutine qubit_owner_op = op.in_qubits[0].owner if not _is_type_convertible(qubit_owner_op, qecl.LogicalCodeblockType): _raise_failed_to_convert_op_compile_error(op) @@ -377,7 +379,7 @@ def match_and_rewrite(self, op: quantum.CustomOp, rewriter: PatternRewriter): (qubit_owner_op.results[0],), (qubit_owner_op.operands[0].type,) ), t_gate_subroutine := func.CallOp( - callee=SymbolRefAttr(self.t_subroutine.sym_name), + callee=SymbolRefAttr(subroutine.sym_name), arguments=conv_cast_op.results[0], return_types=conv_cast_op.results[0].type, ), @@ -900,7 +902,9 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: module_block = op.regions[0].blocks.first assert module_block is not None, "Module has no block" t_subroutine = self.create_t_subroutine() + t_adj_subroutine = self.create_t_subroutine(adj=True) module_block.add_op(t_subroutine) + module_block.add_op(t_adj_subroutine) PatternRewriteWalker( GreedyRewritePatternApplier( @@ -909,7 +913,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: ExtractOpConversion(), InsertOpConversion(), DeallocOpConversion(), - CustomOpConversion(t_subroutine=t_subroutine), + CustomOpConversion(t_subroutine=t_subroutine, t_adj_subroutine=t_adj_subroutine), MeasureOpConversion(), ScfYieldConversion(), ScfIfConversion(), @@ -922,7 +926,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: # this pass removes them ReconcileUnrealizedCastsPass().apply(ctx, op) - def create_t_subroutine(self): + def create_t_subroutine(self, adj=False): """Create a subroutine that takes in a codeblock in state |φ>, and outputs a codeblock in state T|φ>. The subroutine includes instructions to fabricate a codeblock in the magic state, entangle it with the input codeblock and perform measurements and corrections. @@ -951,7 +955,8 @@ def create_t_subroutine(self): (in_codeblock,) = block.args # fabricate aux codeblock in magic state - fabricate_op = qecl.FabricateOp("magic", qecl.LogicalCodeblockType(k=self.k)) + init_state = "magic_conj" if adj else "magic" + fabricate_op = qecl.FabricateOp(init_state, qecl.LogicalCodeblockType(k=self.k)) magic_state_block = fabricate_op.results[0] # apply cnot between aux codeblock and input data codeblock @@ -995,8 +1000,10 @@ def create_t_subroutine(self): if_apply_corr_op.results[0], ) + func_name = "apply_T_adj" if adj else "apply_T" + funcOp = func.FuncOp( - name="apply_T", + name=func_name, function_type=(input_types, output_types), visibility="private", region=Region([block]), diff --git a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py index cb6bc4c8d1..f07750bc92 100644 --- a/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py +++ b/frontend/test/pytest/python_interface/transforms/qecl/test_xdsl_convert_quantum_to_qecl.py @@ -662,9 +662,13 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): // CHECK: [[cb2:%.+]] = qecl.qec [[cb1]] : !qecl.codeblock<1> %2 = quantum.custom "T"() %1 : !quantum.bit - // CHECK: [[conv_cast:%.+]] = builtin.unrealized_conversion_cast [[cb2]] : !qecl.codeblock<1> to !quantum.bit + // CHECK: [[cb3:%.+]] = func.call @apply_T_adj([[cb2]]) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + // CHECK: [[cb4:%.+]] = qecl.qec [[cb3]] : !qecl.codeblock<1> + // CHECK: [[conv_cast:%.+]] = builtin.unrealized_conversion_cast [[cb4]] : !qecl.codeblock<1> to !quantum.bit + %3 = quantum.custom "T"() %2 adj : !quantum.bit + // CHECK: "test.op"([[conv_cast]]) : (!quantum.bit) -> !quantum.bit - %3 = "test.op"(%2) : (!quantum.bit) -> !quantum.bit // To prevent DCE + %4 = "test.op"(%3) : (!quantum.bit) -> !quantum.bit // To prevent DCE return } // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecl.codeblock<1>) @@ -679,6 +683,8 @@ def test_gate_t_k_1(self, run_filecheck, quantum_to_qecl_pipeline_k_1): // CHECK-NEXT: else // CHECK-NEXT: scf.yield [[magic_cb2]] // CHECK: func.return [[out_codeblock]] + // CHECK: func.func private @apply_T_adj([[in_codeblock:%.+]]: !qecl.codeblock<1>) + // CHECK-NEXT: [[magic_cb:%.+]] = qecl.fabricate[magic_conj] : !qecl.codeblock<1> } """ run_filecheck(program, quantum_to_qecl_pipeline_k_1) From 09b29b72e390ed81de399b001dc9e3b0e9ad5a81 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 22:07:40 -0400 Subject: [PATCH 075/120] fix typo in magic_conj attr def --- mlir/include/QecLogical/IR/QecLogicalAttrDefs.td | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td index 00ab72e624..c0c93080d6 100644 --- a/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td +++ b/mlir/include/QecLogical/IR/QecLogicalAttrDefs.td @@ -27,8 +27,8 @@ def LogicalCodeblockInitState : I32EnumAttr<"LogicalCodeblockInitState", "logical codeblock initial state", [ I32EnumAttrCase<"Zero", 0, "zero">, // |0⟩ - I32EnumAttrCase<"Magic", 1, "magic"> // |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) - I32EnumAttrCase<"Magic_conj", 1, "magic_conj"> // |m̄⟩ = |0⟩ + e^{-iπ/4}|1⟩ (Magic state conjugated) + I32EnumAttrCase<"Magic", 1, "magic">, // |m⟩ = |0⟩ + e^{iπ/4}|1⟩ (Magic state) + I32EnumAttrCase<"Magic_conj", 2, "magic_conj"> // |m̄⟩ = |0⟩ + e^{-iπ/4}|1⟩ (Magic state conjugated) ]> { let cppNamespace = "catalyst::qecl"; let genSpecializedAttr = 0; From f00594ae52b96a092e7ca57299afd29a3bce861b Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 22:08:22 -0400 Subject: [PATCH 076/120] add adj property for qecp.t --- frontend/catalyst/python_interface/dialects/qecp.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/catalyst/python_interface/dialects/qecp.py b/frontend/catalyst/python_interface/dialects/qecp.py index 8996e23b21..4da6ca30f1 100644 --- a/frontend/catalyst/python_interface/dialects/qecp.py +++ b/frontend/catalyst/python_interface/dialects/qecp.py @@ -686,9 +686,6 @@ class SOp(SingleQubitPhysicalGateOp): class TOp(SingleQubitPhysicalGateOp): """A physical T gate operation.""" - def __init__(self, in_qubit: QecPhysicalQubitSSAValue | Operation): - super().__init__(in_qubit) - name = "qecp.t" From eb35a9b2662611ce8c3facd0058e755db5b1a50f Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 22:09:20 -0400 Subject: [PATCH 077/120] add lowering and tests for qecl to qecp --- .../transforms/qecp/convert_qecl_to_qecp.py | 124 ++++++++++-------- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 75 +++++++---- 2 files changed, 116 insertions(+), 83 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index cc385be2b3..8ad7319a04 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -221,26 +221,28 @@ class FabricateOpConversion(RewritePattern): """Converts qecl.fabricate to the equivalent subroutine of qecp gates""" qec_code: QecCode - fabricate_subroutine: func.FuncOp + fabricate_subroutines: func.FuncOp @op_type_rewrite_pattern def match_and_rewrite(self, op: qecl.FabricateOp, rewriter: PatternRewriter): - """Rewrite pattern for `qecl.fabricate [magic]` op""" + """Rewrite pattern for `qecl.fabricate` op""" - if not op.init_state.data == "magic": + supported_states = [state for state in self.fabricate_subroutines.keys()] + if op.init_state.data not in supported_states: raise NotImplementedError( "Lowering qecl.FabricateOp to the qecp dialect is only implemented " - "for init_state 'magic'" + f"for the following init_states: {supported_states}" ) - + if (k := op.out_codeblock.type.k.value.data) != self.qec_code.k: raise CompileError( f"Circuit expressed in the qecl dialect with k={k} is not compatible with " f"lowering to a code with k={self.qec_code.k}" ) - - callee = builtin.SymbolRefAttr(self.fabricate_subroutine.sym_name) - return_types = self.fabricate_subroutine.function_type.outputs.data + + subroutine = self.fabricate_subroutines[op.init_state.data] + callee = builtin.SymbolRefAttr(subroutine.sym_name) + return_types = subroutine.function_type.outputs.data callOp = func.CallOp(callee, arguments=(), return_types=return_types) rewriter.replace_op(op, callOp) @@ -481,8 +483,9 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine() module_block.add_op(physical_meas_decode_subroutine) - fabricate_subroutine = self.create_fabricate_subroutine() - module_block.add_op(fabricate_subroutine) + fabricate_subroutines = self.create_fabricate_subroutines() + for subroutine in fabricate_subroutines.values(): + module_block.add_op(subroutine) # 1Q gate and 2Q gate subroutines are returned as dicts storing # {"gate_name": subroutine_funcop} @@ -505,7 +508,7 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: ExtractBlockConversion(), EncodeOpConversion(qec_code=self.qec_code, encode_subroutine=encode_subroutine), FabricateOpConversion( - qec_code=self.qec_code, fabricate_subroutine=fabricate_subroutine + qec_code=self.qec_code, fabricate_subroutines=fabricate_subroutines ), QecCycleOpConversion( qec_code=self.qec_code, qec_cycle_subroutine=qec_cycle_subroutine @@ -808,9 +811,9 @@ def create_encode_subroutine(self) -> func.FuncOp: return funcOp - # MARK: Fabricate subroutine + # MARK: Fabricate subroutines - def create_fabricate_subroutine(self) -> func.FuncOp: + def create_fabricate_subroutines(self) -> func.FuncOp: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a non-fault tolerant encoding intended for use on a simulator, and not a distillation process @@ -835,58 +838,67 @@ def create_fabricate_subroutine(self) -> func.FuncOp: f"(missing {required_keys - set(unitary_encoding_info)}); cannot lower " f"qecl.fabricate [magic] for this code." ) + + subroutines = {} - codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) - input_types = () - output_types = (codeblock_type,) - - block = Block(arg_types=input_types) - - with ImplicitBuilder(block): - codeblock = qecp.AllocCodeblockOp( - codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) - ) - - # extract qubits - extract_ops = [qecp.ExtractQubitOp(codeblock, i) for i in range(self.qec_code.n)] - magic_state_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} + for init_state in ["magic", "magic_conj"]: - # apply H and T to the state prep input qubit - state_prep_index = unitary_encoding_info["state_prep_index"] - h_op = qecp.HadamardOp(magic_state_qubits[state_prep_index]) - t_op = qecp.TOp(h_op.results[0]) - magic_state_qubits[state_prep_index] = t_op.results[0] + codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) + input_types = () + output_types = (codeblock_type,) - # Perform unitary encoding circuit for the code - hadamards = unitary_encoding_info["hadamard_indices"] - cnot_pairs = unitary_encoding_info["cnot_indices"] + block = Block(arg_types=input_types) - for idx in hadamards: - h = qecp.HadamardOp(magic_state_qubits[idx]) - magic_state_qubits[idx] = h.results[0] + with ImplicitBuilder(block): + codeblock = qecp.AllocCodeblockOp( + codeblock_type=qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) + ) - for ctrl_idx, trgt_idx in cnot_pairs: - cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) - magic_state_qubits[ctrl_idx] = cnot_op.results[0] - magic_state_qubits[trgt_idx] = cnot_op.results[1] + # extract qubits + extract_ops = [qecp.ExtractQubitOp(codeblock, i) for i in range(self.qec_code.n)] + magic_state_qubits = {i: ext_op.results[0] for i, ext_op in enumerate(extract_ops)} + + # apply H and T to the state prep input qubit + state_prep_index = unitary_encoding_info["state_prep_index"] + h_op = qecp.HadamardOp(magic_state_qubits[state_prep_index]) + if init_state == "magic": + t_op = qecp.TOp(h_op.results[0]) + elif init_state == "magic_conj": + t_op = qecp.TOp(h_op.results[0], adjoint=True) + magic_state_qubits[state_prep_index] = t_op.results[0] + + # Perform unitary encoding circuit for the code + hadamards = unitary_encoding_info["hadamard_indices"] + cnot_pairs = unitary_encoding_info["cnot_indices"] + + for idx in hadamards: + h = qecp.HadamardOp(magic_state_qubits[idx]) + magic_state_qubits[idx] = h.results[0] + + for ctrl_idx, trgt_idx in cnot_pairs: + cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) + magic_state_qubits[ctrl_idx] = cnot_op.results[0] + magic_state_qubits[trgt_idx] = cnot_op.results[1] + + # insert data qubits back into the codeblock + encoded_codeblock = codeblock + for i in range(self.qec_code.n): + insert_op = qecp.InsertQubitOp(encoded_codeblock, i, magic_state_qubits[i]) + encoded_codeblock = insert_op.results[0] - # insert data qubits back into the codeblock - encoded_codeblock = codeblock - for i in range(self.qec_code.n): - insert_op = qecp.InsertQubitOp(encoded_codeblock, i, magic_state_qubits[i]) - encoded_codeblock = insert_op.results[0] + # return the encoded codeblock + func.ReturnOp(encoded_codeblock) - # return the encoded codeblock - func.ReturnOp(encoded_codeblock) + funcOp = func.FuncOp( + name=f"fabricate_{init_state}_{self.qec_code.name}", + function_type=(input_types, output_types), + visibility="private", + region=Region([block]), + ) - funcOp = func.FuncOp( - name=f"fabricate_magic_state_{self.qec_code.name}", - function_type=(input_types, output_types), - visibility="private", - region=Region([block]), - ) + subroutines[init_state] = funcOp - return funcOp + return subroutines # MARK: 1Q gate subroutines diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index b013a1c47b..d31e7a0878 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1035,7 +1035,8 @@ def test_nontransveral_ops_ignored(self, run_filecheck, get_generic_qec_code): class TestLoweringFabricateOp: """Test lowering for the qecl.fabricate op""" - def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): + @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", "adj")]) + def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_generic_qec_code): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine with a toy code, and that the subroutine performs H-T state injection on the code's state_prep_index followed by the unitary encoding @@ -1052,14 +1053,14 @@ def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): }, ) - program = """ - builtin.module @module_circuit { - func.func @test_func() attributes {quantum.node} { - // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_TestCode() : () -> !qecp.codeblock<1 x 3> - %0 = qecl.fabricate[magic] : !qecl.codeblock<1> + program = f""" + builtin.module @module_circuit {{ + func.func @test_func() attributes {{quantum.node}} {{ + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_{init_state}_TestCode() : () -> !qecp.codeblock<1 x 3> + %0 = qecl.fabricate[{init_state}] : !qecl.codeblock<1> return - } - // CHECK-LABEL: func.func private @fabricate_magic_state_TestCode() -> !qecp.codeblock<1 x 3> { + }} + // CHECK-LABEL: func.func private @fabricate_{init_state}_TestCode() -> !qecp.codeblock<1 x 3> // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 3> // Extract qubits // CHECK: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 3> -> !qecp.qubit @@ -1067,7 +1068,7 @@ def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): // CHECK: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 3> -> !qecp.qubit // State injection on the state_prep_index (q1) // CHECK: [[q1_1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit - // CHECK: [[q1_2:%.+]] = qecp.t [[q1_1]] : !qecp.qubit + // CHECK: [[q1_2:%.+]] = qecp.t [[q1_1]] {adj} : !qecp.qubit // Unitary encoding // CHECK: [[q0_1:%.+]] = qecp.hadamard [[q0]] : !qecp.qubit // CHECK: [[q2_1:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit @@ -1078,26 +1079,26 @@ def test_lower_fabricate_toy_code(self, run_filecheck, get_generic_qec_code): // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_out]] // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_out]] // CHECK: func.return [[cb_3:%.+]] : !qecp.codeblock<1 x 3> - // CHECK: } - } + }} """ pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) run_filecheck(program, pipeline) - def test_fabricate_lowering_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): + @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", "adj")]) + def test_fabricate_lowering_steane(self, init_state, adj, run_filecheck, qecl_to_qecp_steane_pipeline): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine when using the Steane code, and that the subroutine performs H-T state injection on the Steane code's state_prep_index (qubit 6) followed by the unitary encoding.""" - program = """ - builtin.module @module_circuit { - func.func @test_func() attributes {quantum.node} { - // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_Steane() : () -> !qecp.codeblock<1 x 7> - %0 = qecl.fabricate[magic] : !qecl.codeblock<1> + program = f""" + builtin.module @module_circuit {{ + func.func @test_func() attributes {{quantum.node}} {{ + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_{init_state}_Steane() : () -> !qecp.codeblock<1 x 7> + %0 = qecl.fabricate[{init_state}] : !qecl.codeblock<1> return - } - // CHECK-LABEL: func.func private @fabricate_magic_state_Steane() -> !qecp.codeblock<1 x 7> { + }} + // CHECK-LABEL: func.func private @fabricate_magic_Steane() -> !qecp.codeblock<1 x 7> // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 7> // CHECK: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit // CHECK: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit @@ -1108,18 +1109,17 @@ def test_fabricate_lowering_steane(self, run_filecheck, qecl_to_qecp_steane_pipe // CHECK: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 7> -> !qecp.qubit // State injection on the state_prep_index (qubit 6): H then T // CHECK: [[h_inj:%.+]] = qecp.hadamard [[q6]] : !qecp.qubit - // CHECK: [[t_inj:%.+]] = qecp.t [[h_inj]] : !qecp.qubit + // CHECK: [[t_inj:%.+]] = qecp.t [[h_inj]] {adj} : !qecp.qubit // Unitary encoding: Hadamards on indices 1, 2, 3 // CHECK: [[h1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit // CHECK: [[h2:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit // CHECK: [[h3:%.+]] = qecp.hadamard [[q3]] : !qecp.qubit // First few CNOTs of the encoding circuit - // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[h1]], [[q0]] : !qecp.qubit, !qecp.qubit - // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[h2]], [[q4]] : !qecp.qubit, !qecp.qubit - // CHECK: {{%.+}}, {{%.+}} = qecp.cnot [[t_inj]], [[q5]] : !qecp.qubit, !qecp.qubit - // CHECK: func.return {{%.+}} : !qecp.codeblock<1 x 7> - // CHECK: } - } + // CHECK: qecp.cnot [[h1]], [[q0]] : !qecp.qubit, !qecp.qubit + // CHECK: qecp.cnot [[h2]], [[q4]] : !qecp.qubit, !qecp.qubit + // CHECK: qecp.cnot [[t_inj]], [[q5]] : !qecp.qubit, !qecp.qubit + // CHECK: func.return + }} """ run_filecheck(program, qecl_to_qecp_steane_pipeline) @@ -1134,14 +1134,35 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK: [[cb1:%.+]] = func.call @apply_T([[cb0]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> %2 = func.call @apply_T(%0) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + + // CHECK: [[cb2:%.+]] = func.call @apply_T_adj([[cb1]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> + %3 = func.call @apply_T_adj(%2) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> return } - // CHECK: func.func private @apply_T([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) + // CHECK-LABEL: func.func private @apply_T([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) + // CHECK: func.call @fabricate_magic_Steane() : () -> !qecp.codeblock<1 x 7> + // CHECK: qecp.dealloc_cb func.func private @apply_T(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { %1 = qecl.fabricate[magic] : !qecl.codeblock<1> qecl.dealloc_cb %0 : !qecl.codeblock<1> func.return %1 : !qecl.codeblock<1> } + // CHECK-LABEL: func.func private @apply_T_adj([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) + // CHECK: func.call @fabricate_magic_conj_Steane() : () -> !qecp.codeblock<1 x 7> + func.func private @apply_T_adj(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { + %1 = qecl.fabricate[magic_conj] : !qecl.codeblock<1> + } + // CHECK-LABEL: func.func private @fabricate_magic_Steane + // CHECK: qecp.alloc_cb + // CHECK: qecp.h + // CHECK: qecp.t [[qb:%.+]] + // CHECK-NOT: qecp.t [[qb:%.+]] adj + // CHECK: qecp.h + // CHECK: qecp.cnot + // CHECK-LABEL: func.func private @fabricate_magic_conj_Steane + // CHECK: qecp.alloc_cb + // CHECK: qecp.t [[qb:%.+]] adj + // CHECK-NOT: qepc.t [[qb:%.+]] : } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) From b11cd6edcd8abaece8049a099aeee32b6b9b1e13 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Thu, 4 Jun 2026 22:17:12 -0400 Subject: [PATCH 078/120] update tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 39 ++++++++-------- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index d31e7a0878..1fecae0eb5 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1035,7 +1035,7 @@ def test_nontransveral_ops_ignored(self, run_filecheck, get_generic_qec_code): class TestLoweringFabricateOp: """Test lowering for the qecl.fabricate op""" - @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", "adj")]) + @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", " adj")]) def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_generic_qec_code): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine with a toy code, and that the subroutine performs H-T @@ -1068,7 +1068,7 @@ def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_gene // CHECK: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 3> -> !qecp.qubit // State injection on the state_prep_index (q1) // CHECK: [[q1_1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit - // CHECK: [[q1_2:%.+]] = qecp.t [[q1_1]] {adj} : !qecp.qubit + // CHECK: [[q1_2:%.+]] = qecp.t [[q1_1]]{adj} : !qecp.qubit // Unitary encoding // CHECK: [[q0_1:%.+]] = qecp.hadamard [[q0]] : !qecp.qubit // CHECK: [[q2_1:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit @@ -1084,7 +1084,7 @@ def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_gene pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) run_filecheck(program, pipeline) - @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", "adj")]) + @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", " adj")]) def test_fabricate_lowering_steane(self, init_state, adj, run_filecheck, qecl_to_qecp_steane_pipeline): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine when using the Steane code, and that the subroutine performs @@ -1098,27 +1098,26 @@ def test_fabricate_lowering_steane(self, init_state, adj, run_filecheck, qecl_to %0 = qecl.fabricate[{init_state}] : !qecl.codeblock<1> return }} - // CHECK-LABEL: func.func private @fabricate_magic_Steane() -> !qecp.codeblock<1 x 7> + // CHECK-LABEL: func.func private @fabricate_{init_state}_Steane() -> !qecp.codeblock<1 x 7> // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 7> - // CHECK: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 7> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 7> -> !qecp.qubit // State injection on the state_prep_index (qubit 6): H then T - // CHECK: [[h_inj:%.+]] = qecp.hadamard [[q6]] : !qecp.qubit - // CHECK: [[t_inj:%.+]] = qecp.t [[h_inj]] {adj} : !qecp.qubit + // CHECK-NEXT: [[h_inj:%.+]] = qecp.hadamard [[q6]] : !qecp.qubit + // CHECK-NEXT: [[t_inj:%.+]] = qecp.t [[h_inj]]{adj} : !qecp.qubit // Unitary encoding: Hadamards on indices 1, 2, 3 - // CHECK: [[h1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit - // CHECK: [[h2:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit - // CHECK: [[h3:%.+]] = qecp.hadamard [[q3]] : !qecp.qubit + // CHECK-NEXT: [[h1:%.+]] = qecp.hadamard [[q1]] : !qecp.qubit + // CHECK-NEXT: [[h2:%.+]] = qecp.hadamard [[q2]] : !qecp.qubit + // CHECK-NEXT: [[h3:%.+]] = qecp.hadamard [[q3]] : !qecp.qubit // First few CNOTs of the encoding circuit - // CHECK: qecp.cnot [[h1]], [[q0]] : !qecp.qubit, !qecp.qubit - // CHECK: qecp.cnot [[h2]], [[q4]] : !qecp.qubit, !qecp.qubit - // CHECK: qecp.cnot [[t_inj]], [[q5]] : !qecp.qubit, !qecp.qubit - // CHECK: func.return + // CHECK-NEXT: qecp.cnot [[h1]], [[q0]] : !qecp.qubit, !qecp.qubit + // CHECK-NEXT: qecp.cnot [[h2]], [[q4]] : !qecp.qubit, !qecp.qubit + // CHECK-NEXT: qecp.cnot [[t_inj]], [[q5]] : !qecp.qubit, !qecp.qubit }} """ run_filecheck(program, qecl_to_qecp_steane_pipeline) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 538faa89f1..cfa77abf89 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -751,3 +751,48 @@ def circ(): eigenvalues = [-1 if s else 1 for s in samples] # could have a lower atol with more shots, but given test duration, not worth it assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) + + + @pytest.mark.parametrize( + "n, diagonalizing_gates, expected_res, shots", + [ + (1, [qp.H], 0.707, 1000), + (1, [qp.Z, qp.S, qp.H], -0.707, 1000), + # with 2 adj-T gates, expval(Y) is -1 for every shot, so we can use fewer shots + (2, [qp.Z, qp.S, qp.H], -1, 20), + ], + ) + def test_T_adj_gate_integration( + self, n, diagonalizing_gates, expected_res, shots, run_filecheck_qjit + ): + """Integration test for adjoint(T) gates.""" + + dev = qp.device("lightning.qubit", wires=1) + + @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=6) + @convert_qecp_to_quantum_pass + @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) + @inject_noise_to_qecl_pass + @convert_quantum_to_qecl_pass(k=1) + @qp.set_shots(shots) + @qp.qnode(dev, mcm_method="one-shot") + def circ(): + # CHECK: quantum.alloc + # CHECK: func.call @apply_T_adj + # CHECK: fabricate_magic_state_conj_Steane + # CHECK: qecp.assemble_tanner + # CHECK: qecp.decode_esm_css + # CHECK: quantum.custom "Hadamard" + qp.Hadamard(0) + for _ in range(n): + qp.adjoint(qp.T)(0) + for op in diagonalizing_gates: + op(0) + m0 = qp.measure(0) + return qp.sample(m0) + + run_filecheck_qjit(circ) + samples = circ() + eigenvalues = [-1 if s else 1 for s in samples] + # could have a lower atol with more shots, but given test duration, not worth it + assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) From f677735fc49912c3a60115d0ecd9172bbdf6a6da Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 09:17:56 -0400 Subject: [PATCH 079/120] test adjustments --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index cfa77abf89..fea7c518f3 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -734,7 +734,7 @@ def test_T_gate_integration( def circ(): # CHECK: quantum.alloc # CHECK: func.call @apply_T - # CHECK: fabricate_magic_state_Steane + # CHECK: fabricate_magic_Steane # CHECK: qecp.assemble_tanner # CHECK: qecp.decode_esm_css # CHECK: quantum.custom "Hadamard" @@ -759,7 +759,7 @@ def circ(): (1, [qp.H], 0.707, 1000), (1, [qp.Z, qp.S, qp.H], -0.707, 1000), # with 2 adj-T gates, expval(Y) is -1 for every shot, so we can use fewer shots - (2, [qp.Z, qp.S, qp.H], -1, 20), + # (2, [qp.Z, qp.S, qp.H], -1, 20), ], ) def test_T_adj_gate_integration( @@ -779,13 +779,14 @@ def test_T_adj_gate_integration( def circ(): # CHECK: quantum.alloc # CHECK: func.call @apply_T_adj - # CHECK: fabricate_magic_state_conj_Steane + # CHECK: fabricate_magic_conj_Steane # CHECK: qecp.assemble_tanner # CHECK: qecp.decode_esm_css # CHECK: quantum.custom "Hadamard" qp.Hadamard(0) - for _ in range(n): - qp.adjoint(qp.T)(0) + qp.adjoint(qp.T(0)) # adjoint of op + if n==2: + qp.adjoint(qp.T)(0) # adjoint of class - do we want to worry about supporting this? for op in diagonalizing_gates: op(0) m0 = qp.measure(0) From 8ff910256ab999ced8256ad711cbfdcb407f732e Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 18:48:54 -0400 Subject: [PATCH 080/120] update integration test --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 58b6355582..2379758584 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -643,7 +643,7 @@ def ghz(): ([qp.H, qp.Z], (-1, 0, 0)), # adjoint and S adjoint rotate onto y axis in expected directions ([qp.H, qp.S], (0, 1, 0)), - ([qp.H, qp.adjoint(qp.S)], (0, -1, 0)), + ([qp.H, lambda w: qp.adjoint(qp.S(w))], (0, -1, 0)), # hadamard is self-inverse ([qp.H, qp.H], (0, 0, 1)), ], @@ -653,33 +653,52 @@ def test_qec_single_clifford_gate_ops_integration(self, gates, expected_results) dev = qp.device("lightning.qubit", wires=1) - # compile_pipeline = qp.CompilePipeline() + qec_conversion_and_noise_passes = qp.CompilePipeline( + convert_quantum_to_qecl_pass(k=1), + qp.transform(pass_name="symbol-dce"), + inject_noise_to_qecl_pass, + convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1), + convert_qecp_to_quantum_pass, + ) @qp.qjit(capture=True, pipelines=qec_pipeline()) - @convert_qecp_to_quantum_pass - @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) - @inject_noise_to_qecl_pass - @qp.transform(pass_name="symbol-dce") - @convert_quantum_to_qecl_pass(k=1) - @qp.set_shots(1) + @qec_conversion_and_noise_passes + @qp.set_shots(700) @qp.qnode(dev, mcm_method="one-shot") - def test_circ(): + def x_circ(): for gate in gates: gate(0) - # gate(1) - # gate(2) qp.H(0) - # qp.Z(1) - # qp.S(1) - # qp.H(1) m0 = qp.measure(0) - # m1 = qp.measure(1) - # m2 = qp.measure(2) - return qp.sample(m0)#, qp.sample(m1), qp.sample(m2) + return qp.sample(m0) + + @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qec_conversion_and_noise_passes + @qp.set_shots(700) + @qp.qnode(dev, mcm_method="one-shot") + def y_circ(): + for gate in gates: + gate(0) + qp.Z(0) + qp.S(0) + qp.H(0) + m0 = qp.measure(0) + return qp.sample(m0) + + @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qec_conversion_and_noise_passes + @qp.set_shots(700) + @qp.qnode(dev, mcm_method="one-shot") + def z_circ(): + for gate in gates: + gate(0) + m0 = qp.measure(0) + return qp.sample(m0) - all_samples = test_circ() - print(all_samples) - raise RuntimeError() + all_samples = x_circ(), y_circ(), z_circ() - # res = [np.mean([-1 if s else 1 for s in sample]) for sample in all_samples] - # assert np.allclose(res, expected_result) + for samples, res in zip(all_samples, expected_results): + eigvals = [-1 if s else 1 for s in samples] + # the tolerance is a bit high, but it keeps number of shots down + assert np.isclose(np.mean(eigvals), res, atol=0.1) + From 43a748fa3b285cdad7924d2e1a35b446779918d5 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 19:02:27 -0400 Subject: [PATCH 081/120] black formatting --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 2379758584..0b56f92d69 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -658,8 +658,8 @@ def test_qec_single_clifford_gate_ops_integration(self, gates, expected_results) qp.transform(pass_name="symbol-dce"), inject_noise_to_qecl_pass, convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1), - convert_qecp_to_quantum_pass, - ) + convert_qecp_to_quantum_pass, + ) @qp.qjit(capture=True, pipelines=qec_pipeline()) @qec_conversion_and_noise_passes @@ -671,7 +671,7 @@ def x_circ(): qp.H(0) m0 = qp.measure(0) return qp.sample(m0) - + @qp.qjit(capture=True, pipelines=qec_pipeline()) @qec_conversion_and_noise_passes @qp.set_shots(700) @@ -684,7 +684,7 @@ def y_circ(): qp.H(0) m0 = qp.measure(0) return qp.sample(m0) - + @qp.qjit(capture=True, pipelines=qec_pipeline()) @qec_conversion_and_noise_passes @qp.set_shots(700) @@ -701,4 +701,3 @@ def z_circ(): eigvals = [-1 if s else 1 for s in samples] # the tolerance is a bit high, but it keeps number of shots down assert np.isclose(np.mean(eigvals), res, atol=0.1) - From 59db545b08cd6d789bfbebe304fef7d881f0e189 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 19:04:14 -0400 Subject: [PATCH 082/120] codefactor --- .../transforms/qecp/convert_qecl_to_qecp.py | 1 + .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index f83a921f5c..4bdbbbc8c0 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -70,6 +70,7 @@ from .convert_qecl_noise_to_qec_noise import ConvertQECLNoiseOpToQECPNoisePass from .qec_code_lib import QecCode +# pylint: disable=too-many-lines class CheckType(StrEnum): """Check types for QEC codes. Currently limited to CSS codes (X and Z checks).""" diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index dd9eb84e17..0b4cb25dd0 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -926,16 +926,16 @@ def test_adjoint_s_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipe """Test that using the Steane code lowers Hadamard, Identity and S ops as expected. These ops are applied on all qubits in the codeblock. For the S operator, the adjoint is applied.""" - program = f""" - builtin.module @module_circuit {{ - func.func @test_func() attributes {{quantum.node}} {{ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 7> // CHECK-NEXT: [[codeblock2:%.+]] = func.call @s_adj_Steane([[codeblock]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> // CHECK-NOT: qecl.s %0 = "test.op"() : () -> !qecl.codeblock<1> %1 = qecl.s %0[0] adj : !qecl.codeblock<1> return - }} + } // CHECK: func.func private @s_adj_Steane([[codeblock_in:%.+]]: !qecp.codeblock<1 x 7>) // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 7> -> !qecp.qubit // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 7> -> !qecp.qubit @@ -959,7 +959,7 @@ def test_adjoint_s_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipe // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 7>, !qecp.qubit // CHECK-NEXT: func.return [[codeblock_out]] - }} + } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) From dd68db129994e724c0bea69c251848ee4ddb7da9 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 23:21:53 -0400 Subject: [PATCH 083/120] try adjoint correction op --- .../transforms/qecl/convert_quantum_to_qecl.py | 18 ++++++++++++++---- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 5 ++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index 792308f07b..e28aa0cd35 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -943,6 +943,9 @@ def create_t_subroutine(self, adj=False): |input_state> ──╰X──┤↗├──║── deallocate ╚═══╝ + The corresponding subroutine for the adjoint uses a conjugate magic state, and applies + the conditional correction adjoint(SX), i.e. XS† + Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. """ @@ -988,10 +991,17 @@ def create_t_subroutine(self, adj=False): ) with ImplicitBuilder(if_apply_corr_op.true_region): - # the correction operator "SX" is applied via applying its component gates - # right to left, i.e. with X, followed by S - corrected_cb1 = qecl.PauliXOp(magic_state1, idx=0) - corr_cb_out = qecl.SOp(corrected_cb1, idx=0) + # This branch is for the case where a correctable error was detected + if adj: + # the correction operator "XS†" is applied via applying its component gates + # right to left, i.e. with adjoint(S), followed by X + corrected_cb1 = qecl.SOp(magic_state1, idx=0, adjoint=True) + corr_cb_out = qecl.PauliXOp(corrected_cb1, idx=0) + else: + # the correction operator "SX" is applied via applying its component gates + # right to left, i.e. with X, followed by S + corrected_cb1 = qecl.PauliXOp(magic_state1, idx=0) + corr_cb_out = qecl.SOp(corrected_cb1, idx=0) scf.YieldOp(corr_cb_out) with ImplicitBuilder(if_apply_corr_op.false_region): diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index f7c38543ed..5a618994f1 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -862,9 +862,8 @@ def circ(): # CHECK: qecp.decode_esm_css # CHECK: quantum.custom "Hadamard" qp.Hadamard(0) - qp.adjoint(qp.T(0)) # adjoint of op - if n==2: - qp.adjoint(qp.T)(0) # adjoint of class - do we want to worry about supporting this? + for _ in range(n): + qp.adjoint(qp.T(0)) for op in diagonalizing_gates: op(0) m0 = qp.measure(0) From 7b8bbce2bde1e99b8d55ad8d0b4a60d091579668 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 23:41:17 -0400 Subject: [PATCH 084/120] fix correction --- .../qecl/convert_quantum_to_qecl.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index e28aa0cd35..f89c3a4904 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -915,7 +915,9 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: ExtractOpConversion(), InsertOpConversion(), DeallocOpConversion(), - CustomOpConversion(t_subroutine=t_subroutine, t_adj_subroutine=t_adj_subroutine), + CustomOpConversion( + t_subroutine=t_subroutine, t_adj_subroutine=t_adj_subroutine + ), MeasureOpConversion(), ScfYieldConversion(), ScfIfConversion(), @@ -943,9 +945,9 @@ def create_t_subroutine(self, adj=False): |input_state> ──╰X──┤↗├──║── deallocate ╚═══╝ - The corresponding subroutine for the adjoint uses a conjugate magic state, and applies - the conditional correction adjoint(SX), i.e. XS† - + The corresponding subroutine for T-adjoint uses a conjugate magic state, and applies + S-adjoint instead of S in the correction. + Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. """ @@ -991,17 +993,11 @@ def create_t_subroutine(self, adj=False): ) with ImplicitBuilder(if_apply_corr_op.true_region): - # This branch is for the case where a correctable error was detected - if adj: - # the correction operator "XS†" is applied via applying its component gates - # right to left, i.e. with adjoint(S), followed by X - corrected_cb1 = qecl.SOp(magic_state1, idx=0, adjoint=True) - corr_cb_out = qecl.PauliXOp(corrected_cb1, idx=0) - else: - # the correction operator "SX" is applied via applying its component gates - # right to left, i.e. with X, followed by S - corrected_cb1 = qecl.PauliXOp(magic_state1, idx=0) - corr_cb_out = qecl.SOp(corrected_cb1, idx=0) + # the correction operator "SX" is applied via applying its component gates + # right to left, i.e. with X, followed by S + # for the adjoint_T subroutine, S is adjoint + corrected_cb1 = qecl.PauliXOp(magic_state1, idx=0) + corr_cb_out = qecl.SOp(corrected_cb1, idx=0, adjoint=adj) scf.YieldOp(corr_cb_out) with ImplicitBuilder(if_apply_corr_op.false_region): From ac6cd07e495f2356768d00a7e9c5f481cca05788 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 12:25:15 -0400 Subject: [PATCH 085/120] black formatting --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index cbca72a495..de979488e8 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -783,7 +783,7 @@ def z_circ(): eigvals = [-1 if s else 1 for s in samples] # the tolerance is a bit high, but it keeps number of shots down assert np.isclose(np.mean(eigvals), res, atol=0.1) - + # pylint: disable=too-many-positional-arguments, too-many-arguments @pytest.mark.parametrize( "n, diagonalizing_gates, expected_res, shots", From c56276111b73ab235851493f40f1a7f9648cecda Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 12:33:14 -0400 Subject: [PATCH 086/120] update tests --- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 4 ++-- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 76710014ee..69e3579647 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -941,8 +941,8 @@ def test_single_qubit_gate_lowering_Steane( run_filecheck(program, qecl_to_qecp_steane_pipeline) def test_adjoint_s_lowering_Steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): - """Test that using the Steane code lowers Hadamard, Identity and S ops as expected. These ops - are applied on all qubits in the codeblock. For the S operator, the adjoint is applied.""" + """Test that using the Steane code lowers S adjoint as expected the physical S operator + is applied to all qubits in the codeblock).""" program = """ builtin.module @module_circuit { diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index de979488e8..71e80b62d4 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -743,7 +743,7 @@ def test_qec_single_clifford_gate_ops_integration(self, gates, expected_results) convert_qecp_to_quantum_pass, ) - @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=123) @qec_conversion_and_noise_passes @qp.set_shots(700) @qp.qnode(dev, mcm_method="one-shot") @@ -754,7 +754,7 @@ def x_circ(): m0 = qp.measure(0) return qp.sample(m0) - @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=456) @qec_conversion_and_noise_passes @qp.set_shots(700) @qp.qnode(dev, mcm_method="one-shot") @@ -767,7 +767,7 @@ def y_circ(): m0 = qp.measure(0) return qp.sample(m0) - @qp.qjit(capture=True, pipelines=qec_pipeline()) + @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=789) @qec_conversion_and_noise_passes @qp.set_shots(700) @qp.qnode(dev, mcm_method="one-shot") From f27b5d017b761dc8a020b25185f571bdfdf118fa Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 12:33:30 -0400 Subject: [PATCH 087/120] update changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 72ce8537f4..9a72dce9ad 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -142,6 +142,7 @@ Physical (`qecp`) dialect. [(#2776)](https://github.com/PennyLaneAI/catalyst/pull/2776) [(#2871)](https://github.com/PennyLaneAI/catalyst/pull/2871) + [(#2922)](https://github.com/PennyLaneAI/catalyst/pull/2922) * The experimental compiler pass `convert-quantum-to-qecl` has been extended to lower `quantum.custom "T"` gates to the `qecl` layer as a subroutine using a magic state. From bd273d224123fe6f09bfa8e13fad4b1854605c89 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Fri, 5 Jun 2026 23:44:42 -0400 Subject: [PATCH 088/120] add reference --- .../transforms/qecl/convert_quantum_to_qecl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py index f89c3a4904..f4d7c76b67 100644 --- a/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py +++ b/frontend/catalyst/python_interface/transforms/qecl/convert_quantum_to_qecl.py @@ -946,7 +946,9 @@ def create_t_subroutine(self, adj=False): ╚═══╝ The corresponding subroutine for T-adjoint uses a conjugate magic state, and applies - S-adjoint instead of S in the correction. + S-adj instead of S in the correction. The correction is derived in Zhou, Leung & Chuang, + Phys. Rev. A 62, 052316 (2000), arXiv:quant-ph/0002039, Sec. IV.A, Eqs. (13)-(15). + Replacing T in these equations with T-adjoint yields the correction used here. Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. From 486b01ee3cfe1d169a089aac613feebfd3617d53 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 14:40:48 -0400 Subject: [PATCH 089/120] update changelog --- doc/releases/changelog-dev.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 9a72dce9ad..b0933b87f0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -144,9 +144,10 @@ [(#2871)](https://github.com/PennyLaneAI/catalyst/pull/2871) [(#2922)](https://github.com/PennyLaneAI/catalyst/pull/2922) -* The experimental compiler pass `convert-quantum-to-qecl` has been extended to lower - `quantum.custom "T"` gates to the `qecl` layer as a subroutine using a magic state. +* The experimental compiler pass `convert-quantum-to-qecl` has been extended to lower the + `quantum.custom "T"` gate to the `qecl` layer as a subroutine using a magic state (or conjugate magic state in the case of the adjoint). [(#2870)](https://github.com/PennyLaneAI/catalyst/pull/2870) + [(#2921)](https://github.com/PennyLaneAI/catalyst/pull/2921) * The reference semantics Pauli Product Measurement operation `pbc.ref.ppm` was added. [(#2773)](https://github.com/PennyLaneAI/catalyst/pull/2773) From c9b434d9b0864ba1fc15ec46143936af0b12943d Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:04:12 -0400 Subject: [PATCH 090/120] add magic_conj dialect tests --- .../python_interface/dialects/test_qecl_dialect.py | 5 ++++- mlir/test/QecLogical/DialectTest.mlir | 7 +++++++ mlir/test/QecLogical/VerifierTest.mlir | 10 +++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py index 0a06ba7b6b..8682db2f38 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py @@ -196,7 +196,7 @@ def test_qecl_op_constructor_insert_block(self, idx, assert_valid_idx_attr): assert_valid_idx_attr(insert_block_op, idx) - @pytest.mark.parametrize("init_state", ["magic", qecl.LogicalCodeblockInitStateAttr("magic")]) + @pytest.mark.parametrize("init_state", ["magic", qecl.LogicalCodeblockInitStateAttr("magic"), "magic_conj", qecl.LogicalCodeblockInitStateAttr("magic_conj")]) def test_qecl_op_constructor_fabricate(self, init_state): """Test the constructor of the qecl.fabricate op.""" fabricate_op = qecl.FabricateOp( @@ -321,6 +321,9 @@ def test_assembly_format(run_filecheck, pretty_print): // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic] : !qecl.codeblock<1> %magic = qecl.fabricate [magic] : !qecl.codeblock<1> + // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic_conj] : !qecl.codeblock<1> + %magic_conj = qecl.fabricate [magic_conj] : !qecl.codeblock<1> + // CHECK: qecl.dealloc_cb [[magic]] : !qecl.codeblock<1> qecl.dealloc_cb %magic : !qecl.codeblock<1> diff --git a/mlir/test/QecLogical/DialectTest.mlir b/mlir/test/QecLogical/DialectTest.mlir index 746df9fbae..221dad1add 100644 --- a/mlir/test/QecLogical/DialectTest.mlir +++ b/mlir/test/QecLogical/DialectTest.mlir @@ -76,6 +76,13 @@ func.func @test_fabricate() { // ----- +func.func @test_fabricate_conj() { + %0 = qecl.fabricate [magic_conj] : !qecl.codeblock<1> + func.return +} + +// ----- + func.func @test_dealloc_cb(%arg0 : !qecl.codeblock<1>) { qecl.dealloc_cb %arg0 : !qecl.codeblock<1> func.return diff --git a/mlir/test/QecLogical/VerifierTest.mlir b/mlir/test/QecLogical/VerifierTest.mlir index e8701c4484..5be62bad8d 100644 --- a/mlir/test/QecLogical/VerifierTest.mlir +++ b/mlir/test/QecLogical/VerifierTest.mlir @@ -87,13 +87,21 @@ func.func @test_fabricate_zero() { // ----- func.func @test_encode_magic(%arg0 : !qecl.codeblock<1>) { - // expected-error@below {{cannot encode a logical codeblock to the magic state}} + // expected-error@below {{cannot encode a logical codeblock to a magic state}} %0 = qecl.encode [magic] %arg0 : !qecl.codeblock<1> func.return } // ----- +func.func @test_encode_magic_conj(%arg0 : !qecl.codeblock<1>) { + // expected-error@below {{cannot encode a logical codeblock to a magic state}} + %0 = qecl.encode [magic_conj] %arg0 : !qecl.codeblock<1> + func.return +} + +// ----- + func.func @test_gate_op_index_out_of_bounds_identity(%b : !qecl.codeblock<1>) { // expected-error@below {{out-of-bounds index attribute}} %b1 = qecl.identity %b[1] : !qecl.codeblock<1> From 7aca6044e3210542a940c5c743467fb7098c9c50 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:04:31 -0400 Subject: [PATCH 091/120] add verifier info --- mlir/lib/QecLogical/IR/QecLogicalOps.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp index 12627777d5..123fde1a95 100644 --- a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp +++ b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp @@ -105,8 +105,8 @@ LogicalResult FabricateOp::verify() LogicalResult EncodeOp::verify() { auto initState = getInitState(); - if (initState == LogicalCodeblockInitState::Magic) { - return emitOpError() << "cannot encode a logical codeblock to the magic state, use '" + if (initState == LogicalCodeblockInitState::Magic || LogicalCodeblockInitState::Magic_conj) { + return emitOpError() << "cannot encode a logical codeblock to a magic state, use '" << FabricateOp::getOperationName() << "' instead."; } return success(); From 93f297efb97360e29d26706054623d47c976cae2 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:07:48 -0400 Subject: [PATCH 092/120] black formatting --- .../transforms/qecp/convert_qecl_to_qecp.py | 16 +++++++++------- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 4 +++- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 1 - 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 56b4843d84..ac1bbd76bd 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -230,19 +230,19 @@ def match_and_rewrite(self, op: qecl.FabricateOp, rewriter: PatternRewriter): """Rewrite pattern for `qecl.fabricate` op""" supported_states = [state for state in self.fabricate_subroutines.keys()] - if op.init_state.data not in supported_states: # pragma: no cover + if op.init_state.data not in supported_states: # pragma: no cover raise NotImplementedError( "Lowering qecl.FabricateOp to the qecp dialect is only implemented " f"for the following init_states: {supported_states}" ) - - if (k := op.out_codeblock.type.k.value.data) != self.qec_code.k: # pragma: no cover + + if (k := op.out_codeblock.type.k.value.data) != self.qec_code.k: # pragma: no cover raise CompileError( f"Circuit expressed in the qecl dialect with k={k} is not compatible with " f"lowering to a code with k={self.qec_code.k}" ) - - subroutine = self.fabricate_subroutines[op.init_state.data] + + subroutine = self.fabricate_subroutines[op.init_state.data] callee = builtin.SymbolRefAttr(subroutine.sym_name) return_types = subroutine.function_type.outputs.data callOp = func.CallOp(callee, arguments=(), return_types=return_types) @@ -849,7 +849,7 @@ def create_fabricate_subroutines(self) -> func.FuncOp: f"(missing {required_keys - set(unitary_encoding_info)}); cannot lower " f"qecl.fabricate [magic] for this code." ) - + subroutines = {} for init_state in ["magic", "magic_conj"]: @@ -887,7 +887,9 @@ def create_fabricate_subroutines(self) -> func.FuncOp: magic_state_qubits[idx] = h.results[0] for ctrl_idx, trgt_idx in cnot_pairs: - cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) + cnot_op = qecp.CnotOp( + magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx] + ) magic_state_qubits[ctrl_idx] = cnot_op.results[0] magic_state_qubits[trgt_idx] = cnot_op.results[1] diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 323fb8f867..a88fd2abae 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1127,7 +1127,9 @@ def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_gene run_filecheck(program, pipeline) @pytest.mark.parametrize("init_state, adj", [("magic", ""), ("magic_conj", " adj")]) - def test_fabricate_lowering_steane(self, init_state, adj, run_filecheck, qecl_to_qecp_steane_pipeline): + def test_fabricate_lowering_steane( + self, init_state, adj, run_filecheck, qecl_to_qecp_steane_pipeline + ): """Test that `qecl.fabricate [magic]` op lowers to a call to the magic-state fabrication subroutine when using the Steane code, and that the subroutine performs H-T state injection on the Steane code's state_prep_index (qubit 6) followed by the diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 53e283b3db..366b907fa9 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -830,7 +830,6 @@ def circ(): # could have a lower atol with more shots, but given test duration, not worth it assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) - @pytest.mark.parametrize( "n, diagonalizing_gates, expected_res, shots", [ From 920c0d4a7f8b37b3078bf958b4a26eb1db97ccfe Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:12:51 -0400 Subject: [PATCH 093/120] cleaning up --- .../transforms/qecp/convert_qecl_to_qecp.py | 6 +++--- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index ac1bbd76bd..fc346dd0e6 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -223,13 +223,13 @@ class FabricateOpConversion(RewritePattern): """Converts qecl.fabricate to the equivalent subroutine of qecp gates""" qec_code: QecCode - fabricate_subroutines: func.FuncOp + fabricate_subroutines: dict[str, func.FuncOp] @op_type_rewrite_pattern def match_and_rewrite(self, op: qecl.FabricateOp, rewriter: PatternRewriter): """Rewrite pattern for `qecl.fabricate` op""" - supported_states = [state for state in self.fabricate_subroutines.keys()] + supported_states = list(self.fabricate_subroutines) if op.init_state.data not in supported_states: # pragma: no cover raise NotImplementedError( "Lowering qecl.FabricateOp to the qecp dialect is only implemented " @@ -819,7 +819,7 @@ def create_encode_subroutine(self) -> func.FuncOp: # MARK: Fabricate subroutines - def create_fabricate_subroutines(self) -> func.FuncOp: + def create_fabricate_subroutines(self) -> dict[str, func.FuncOp]: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a non-fault tolerant encoding intended for use on a simulator, and not a distillation process diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index a88fd2abae..ca528b6e52 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1194,6 +1194,8 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK: func.call @fabricate_magic_conj_Steane() : () -> !qecp.codeblock<1 x 7> func.func private @apply_T_adj(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { %1 = qecl.fabricate[magic_conj] : !qecl.codeblock<1> + qecl.dealloc_cb %0 : !qecl.codeblock<1> + func.return %1 : !qecl.codeblock<1> } // CHECK-LABEL: func.func private @fabricate_magic_Steane // CHECK: qecp.alloc_cb @@ -1205,7 +1207,7 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK-LABEL: func.func private @fabricate_magic_conj_Steane // CHECK: qecp.alloc_cb // CHECK: qecp.t [[qb:%.+]] adj - // CHECK-NOT: qepc.t [[qb:%.+]] : + // CHECK-NOT: qecp.t [[qb:%.+]] : } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) From 5b77ae89b55fefd49303cb2647dee56c246995ca Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:14:30 -0400 Subject: [PATCH 094/120] black formatting more --- .../python_interface/dialects/test_qecl_dialect.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py index 8682db2f38..0fa9a43e87 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py @@ -196,7 +196,15 @@ def test_qecl_op_constructor_insert_block(self, idx, assert_valid_idx_attr): assert_valid_idx_attr(insert_block_op, idx) - @pytest.mark.parametrize("init_state", ["magic", qecl.LogicalCodeblockInitStateAttr("magic"), "magic_conj", qecl.LogicalCodeblockInitStateAttr("magic_conj")]) + @pytest.mark.parametrize( + "init_state", + [ + "magic", + qecl.LogicalCodeblockInitStateAttr("magic"), + "magic_conj", + qecl.LogicalCodeblockInitStateAttr("magic_conj"), + ], + ) def test_qecl_op_constructor_fabricate(self, init_state): """Test the constructor of the qecl.fabricate op.""" fabricate_op = qecl.FabricateOp( From 375756f1581a0190b005a8558889f364a10c44dd Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 15:20:20 -0400 Subject: [PATCH 095/120] pylint --- .../transforms/qecp/convert_qecl_to_qecp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index fc346dd0e6..f383c09eb7 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -871,12 +871,12 @@ def create_fabricate_subroutines(self) -> dict[str, func.FuncOp]: # apply H and T to the state prep input qubit state_prep_index = unitary_encoding_info["state_prep_index"] - h_op = qecp.HadamardOp(magic_state_qubits[state_prep_index]) + next_op = qecp.HadamardOp(magic_state_qubits[state_prep_index]) if init_state == "magic": - t_op = qecp.TOp(h_op.results[0]) + next_op = qecp.TOp(next_op.results[0]) elif init_state == "magic_conj": - t_op = qecp.TOp(h_op.results[0], adjoint=True) - magic_state_qubits[state_prep_index] = t_op.results[0] + next_op = qecp.TOp(next_op.results[0], adjoint=True) + magic_state_qubits[state_prep_index] = next_op.results[0] # Perform unitary encoding circuit for the code hadamards = unitary_encoding_info["hadamard_indices"] From be31d65c306f61c7634b31d78b63debc59888f89 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 16:02:57 -0400 Subject: [PATCH 096/120] fix mistake --- mlir/lib/QecLogical/IR/QecLogicalOps.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp index 123fde1a95..9ab30cc6d7 100644 --- a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp +++ b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp @@ -105,7 +105,7 @@ LogicalResult FabricateOp::verify() LogicalResult EncodeOp::verify() { auto initState = getInitState(); - if (initState == LogicalCodeblockInitState::Magic || LogicalCodeblockInitState::Magic_conj) { + if (initState == LogicalCodeblockInitState::Magic || initState == LogicalCodeblockInitState::Magic_conj) { return emitOpError() << "cannot encode a logical codeblock to a magic state, use '" << FabricateOp::getOperationName() << "' instead."; } From 17d6b0de5cee58fd3c46b1285d37bc0f334c73c1 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 16:06:20 -0400 Subject: [PATCH 097/120] clang formatting --- mlir/lib/QecLogical/IR/QecLogicalOps.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp index 9ab30cc6d7..a100b3dcfe 100644 --- a/mlir/lib/QecLogical/IR/QecLogicalOps.cpp +++ b/mlir/lib/QecLogical/IR/QecLogicalOps.cpp @@ -105,7 +105,8 @@ LogicalResult FabricateOp::verify() LogicalResult EncodeOp::verify() { auto initState = getInitState(); - if (initState == LogicalCodeblockInitState::Magic || initState == LogicalCodeblockInitState::Magic_conj) { + if (initState == LogicalCodeblockInitState::Magic || + initState == LogicalCodeblockInitState::Magic_conj) { return emitOpError() << "cannot encode a logical codeblock to a magic state, use '" << FabricateOp::getOperationName() << "' instead."; } From 0f5aac1f8572d87d98004e45b5de5fdbe421cf16 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 17:31:59 -0400 Subject: [PATCH 098/120] add outline of code --- .../transforms/qecp/qec_code_lib.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index c51b72a08d..89211ab524 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -23,10 +23,9 @@ from catalyst.python_interface.dialects import qecp _CODE_REGISTRY: dict[str, tuple[Any, ...]] = { - # the indices/ordering for the operators and encodings in the Steane code are those used - # in https://arxiv.org/pdf/2107.07505 - "Steane": ( - 7, + # add ref + "Shor913": ( + 9, 1, 3, np.array([[1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 0, 1, 1]]), @@ -71,6 +70,39 @@ "state_prep_index": 6, }, ), + # the indices/ordering for the operators and encodings in the Steane code are those used + # in https://arxiv.org/pdf/2107.07505 + "Steane": ( + 7, + 1, + 3, + np.array([[1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1]]), + np.array([[1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1]]), + # Z is applied transversally using physial X, and vice versa. + # There are no transversal phase gates for this code. + {"x": (qecp.PauliZOp, range(9)), "z": (qecp.PauliXOp, range(9))}, + {"cnot": qecp.CnotOp}, + { + "hadamard_indices": (1, 2, 3), + "cnot_indices": ( + [1, 0], + [2, 4], + [6, 5], + [2, 0], + [3, 5], + [6, 4], + [2, 6], + [3, 4], + [1, 5], + [1, 6], + [3, 0], + ), + # The state_prep_index is the index of the physical qubit that the state is + # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). + # Add reference + "state_prep_index": 0, + }, + ), } From eab2edafce9a15bd79dc77250485cea6aa8f620c Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 17:35:20 -0400 Subject: [PATCH 099/120] fix lit-test mistake --- .../test/pytest/python_interface/dialects/test_qecl_dialect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py index 0fa9a43e87..6437326026 100644 --- a/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py +++ b/frontend/test/pytest/python_interface/dialects/test_qecl_dialect.py @@ -329,7 +329,7 @@ def test_assembly_format(run_filecheck, pretty_print): // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic] : !qecl.codeblock<1> %magic = qecl.fabricate [magic] : !qecl.codeblock<1> - // CHECK: [[magic:%.+]] = qecl.fabricate{{\s*}}[magic_conj] : !qecl.codeblock<1> + // CHECK: [[magic_conj:%.+]] = qecl.fabricate{{\s*}}[magic_conj] : !qecl.codeblock<1> %magic_conj = qecl.fabricate [magic_conj] : !qecl.codeblock<1> // CHECK: qecl.dealloc_cb [[magic]] : !qecl.codeblock<1> From c5f4b75f4a9b2a1d820330facff884c1100b9bf1 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 21:41:44 -0400 Subject: [PATCH 100/120] adjust defining for encoding circuit --- .../transforms/qecp/convert_qecl_to_qecp.py | 33 ++++--- .../transforms/qecp/qec_code_lib.py | 95 +++++++++++-------- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index b35689cde5..02f7452e67 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -839,7 +839,7 @@ def create_fabricate_subroutine(self) -> func.FuncOp: """ unitary_encoding_info = self.qec_code.unitary_encoding - required_keys = {"state_prep_index", "hadamard_indices", "cnot_indices"} + required_keys = {"state_prep_index", "ops"} if not required_keys.issubset(unitary_encoding_info): # pragma: no cover raise CompileError( f"QEC code '{self.qec_code.name}' does not define a unitary encoding " @@ -869,17 +869,28 @@ def create_fabricate_subroutine(self) -> func.FuncOp: magic_state_qubits[state_prep_index] = t_op.results[0] # Perform unitary encoding circuit for the code - hadamards = unitary_encoding_info["hadamard_indices"] - cnot_pairs = unitary_encoding_info["cnot_indices"] - for idx in hadamards: - h = qecp.HadamardOp(magic_state_qubits[idx]) - magic_state_qubits[idx] = h.results[0] - - for ctrl_idx, trgt_idx in cnot_pairs: - cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) - magic_state_qubits[ctrl_idx] = cnot_op.results[0] - magic_state_qubits[trgt_idx] = cnot_op.results[1] + # def cnot_fn(ctrl_idx, trgt_ids): + # cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) + # magic_state_qubits[ctrl_idx] = cnot_op.results[0] + # magic_state_qubits[trgt_idx] = cnot_op.results[1] + + # # hadamards = unitary_encoding_info["hadamard_indices"] + # # cnot_pairs = unitary_encoding_info["cnot_indices"] + + # # for idx in hadamards: + # # h = qecp.HadamardOp(magic_state_qubits[idx]) + # # magic_state_qubits[idx] = h.results[0] + + # # for ctrl_idx, trgt_idx in cnot_pairs: + # # cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) + # # magic_state_qubits[ctrl_idx] = cnot_op.results[0] + # # magic_state_qubits[trgt_idx] = cnot_op.results[1] + + for op, wire_idxs in unitary_encoding_info["ops"]: + op_out = op(*[magic_state_qubits[idx] for idx in wire_idxs]) + for i, idx in enumerate(wire_idxs): + magic_state_qubits[idx] = op_out.results[i] # insert data qubits back into the codeblock encoded_codeblock = codeblock diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 89211ab524..d74ba46b56 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -23,9 +23,10 @@ from catalyst.python_interface.dialects import qecp _CODE_REGISTRY: dict[str, tuple[Any, ...]] = { - # add ref - "Shor913": ( - 9, + # the indices/ordering for the operators and encodings in the Steane code are those used + # in https://arxiv.org/pdf/2107.07505 + "Steane": ( + 7, 1, 3, np.array([[1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 0, 1, 1]]), @@ -36,10 +37,7 @@ # values are a tuple of the qecp gate, and the indices its applied at in the codeblock # will need to be refactored for k>1 "x": (qecp.PauliXOp, [4, 5, 6]), - "y": ( - qecp.PauliYOp, - [4, 5, 6], - ), + "y": (qecp.PauliYOp, [4, 5, 6]), "z": (qecp.PauliZOp, [4, 5, 6]), "hadamard": (qecp.HadamardOp, [0, 1, 2, 3, 4, 5, 6]), "s": (partial(qecp.SOp, adjoint=True), [0, 1, 2, 3, 4, 5, 6]), @@ -49,20 +47,26 @@ "cnot": qecp.CnotOp, }, { - "hadamard_indices": (1, 2, 3), - "cnot_indices": ( - [1, 0], - [2, 4], - [6, 5], - [2, 0], - [3, 5], - [6, 4], - [2, 6], - [3, 4], - [1, 5], - [1, 6], - [3, 0], - ), + # ops (in the form of a qecp operator and the indices of the codeblock + # it should be applied on) defining a transporter encoding circuit, i.e. + # one that maps an input to the logical version of that input, rather + # than just encoding logical 0 + "ops": [ + (qecp.HadamardOp, [1]), + (qecp.HadamardOp, [2]), + (qecp.HadamardOp, [3]), + (qecp.CnotOp, [1, 0]), + (qecp.CnotOp, [2, 4]), + (qecp.CnotOp, [6, 5]), + (qecp.CnotOp, [2, 0]), + (qecp.CnotOp, [3, 5]), + (qecp.CnotOp, [6, 4]), + (qecp.CnotOp, [2, 6]), + (qecp.CnotOp, [3, 4]), + (qecp.CnotOp, [1, 5]), + (qecp.CnotOp, [1, 6]), + (qecp.CnotOp, [3, 0]), + ], # The state_prep_index is the index of the physical qubit that the state is # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). # Must be consistent with the qubit treated as the encoding "input" by the @@ -70,33 +74,40 @@ "state_prep_index": 6, }, ), - # the indices/ordering for the operators and encodings in the Steane code are those used - # in https://arxiv.org/pdf/2107.07505 - "Steane": ( - 7, + # add ref + "Shor913": ( + 9, 1, 3, np.array([[1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1]]), - np.array([[1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1]]), - # Z is applied transversally using physial X, and vice versa. + np.array( + [ + [1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 2, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1], + ] + ), + # Z is applied transversally using physial X, and vice versa. # There are no transversal phase gates for this code. - {"x": (qecp.PauliZOp, range(9)), "z": (qecp.PauliXOp, range(9))}, + {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, {"cnot": qecp.CnotOp}, { - "hadamard_indices": (1, 2, 3), - "cnot_indices": ( - [1, 0], - [2, 4], - [6, 5], - [2, 0], - [3, 5], - [6, 4], - [2, 6], - [3, 4], - [1, 5], - [1, 6], - [3, 0], - ), + "ops": [ + (qecp.CnotOp, [0, 3]), + (qecp.CnotOp, [0, 6]), + (qecp.HadamardOp, [0]), + (qecp.HadamardOp, [3]), + (qecp.HadamardOp, [6]), + (qecp.CnotOp, [0, 1]), + (qecp.CnotOp, [0, 2]), + (qecp.CnotOp, [3, 4]), + (qecp.CnotOp, [3, 5]), + (qecp.CnotOp, [6, 7]), + (qecp.CnotOp, [6, 8]), + ], # The state_prep_index is the index of the physical qubit that the state is # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). # Add reference From 0cd0216120951e05aa8df82a7b66a9d310fa36cc Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 22:35:16 -0400 Subject: [PATCH 101/120] draft tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 153ad5b424..23ab70d80a 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1277,3 +1277,80 @@ def circuit(): return qp.sample([m0]) run_filecheck_qjit(circuit) + + +# MARK: Extensibility + +class TestExtensibility(): + """Test the extensibility to other k=1 CSS codes by testing compilation with the Shor-913 code. + Note that this code does not support any transversal phase gates. These tests check + lowering to the qecp dialect, rather than execution and validity of results.""" + + def test_transversal_gates(self): + """ToDo: docstring. Note we add no noise.""" + + dev = qp.device("lightning.qubit", wires=1) + pipe = [("pipe", ["quantum-compilation-stage"])] + + @qp.qjit(capture=True, pipelines=pipe, target="mlir") + @convert_qecl_to_qecp_pass(qec_code="Shor913", number_errors=0) + @convert_quantum_to_qecl_pass(k=1) + @qp.set_shots(1000) + @qp.qnode(dev, mcm_method="one-shot") + def circ(): + qp.X(0) + qp.Z(1) + qp.CNOT([0, 1]) + m0 = qp.measure(0) + m1 = qp.measure(1) + return qp.sample([m0, m1]) + + mlir = circ.mlir_opt + print(mlir) + raise RuntimeError + + def test_fabricate_magic_state_shor(self, run_filecheck): + """Todo. Note that this has to be a lit test for fabricate op, because without transversal + S, we can't run the apply_T subroutine""" + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_Shor913() : () -> !qecp.codeblock<1 x 9> + %0 = qecl.fabricate[magic] : !qecl.codeblock<1> + return + } + // CHECK-LABEL: func.func private @fabricate_magic_state_Shor913() -> !qecp.codeblock<1 x 9> + // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 9> + // CHECK-DAG: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q7:%.+]] = qecp.extract [[cb]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q8:%.+]] = qecp.extract [[cb]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // State injection on the state_prep_index (qubit 6): H then T + // CHECK: [[h_inj:%.+]] = qecp.hadamard [[q0]] : !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.t [[h_inj]] : !qecp.qubit + // Unitary encoding: initial CNOTs + // CHECK: [[q0_2:%.+]], [[q3_1:%.+]] = qecp.cnot [[q0_1]], [[q3]] : !qecp.qubit, !qecp.qubit + // CHECK: [[q0_3:%.+]], [[q6_1:%.+]] = qecp.cnot [[q0_2]], [[q6]] : !qecp.qubit, !qecp.qubit + // Unitary encoing: Hadamards on indices 0, 3, 6 + // CHECK: [[q0_4:%.+]] = qecp.hadamard [[q0_3]] : !qecp.qubit + // CHECK: [[q3_2:%.+]] = qecp.hadamard [[q3_1]] : !qecp.qubit + // CHECK: [[q6_2:%.+]] = qecp.hadamard [[q6_1]] : !qecp.qubit + // Unitary encoding: more CNOTs - [n, n+1] and [n, n+2] for n in [0, 3, 6] + // CHECK: [[q0_5:%.+]], [[q1_1:%.+]] = qecp.cnot [[q0_4]], [[q1]] : !qecp.qubit, !qecp.qubit + // CHECK: qecp.cnot [[q0_5]], [[q2]] : !qecp.qubit, !qecp.qubit + // CHECK: [[q3_3:%.+]], [[q4_1:%.+]] = qecp.cnot [[q3_2]], [[q4]] : !qecp.qubit, !qecp.qubit + // CHECK: qecp.cnot [[q3_3]], [[q5]] : !qecp.qubit, !qecp.qubit + // CHECK: [[q6_3:%.+]], [[q7_1:%.+]] = qecp.cnot [[q6_2]], [[q7]] : !qecp.qubit, !qecp.qubit + // CHECK: qecp.cnot [[q6_3]], [[q8]] : !qecp.qubit, !qecp.qubit + + } + """ + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) + From 452a9c24831fee1c9f2ca64b96b59055c8d2a8a2 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 22:35:38 -0400 Subject: [PATCH 102/120] update puali indices --- .../catalyst/python_interface/transforms/qecp/qec_code_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index d74ba46b56..ae95d0ba6c 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -92,7 +92,7 @@ ), # Z is applied transversally using physial X, and vice versa. # There are no transversal phase gates for this code. - {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, + {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 3, 6])}, {"cnot": qecp.CnotOp}, { "ops": [ From 0eb1b7341d8935033d347de4564989d2692228cc Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 8 Jun 2026 22:41:19 -0400 Subject: [PATCH 103/120] remove commented out code --- .../transforms/qecp/convert_qecl_to_qecp.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 02f7452e67..cf4188c534 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -869,24 +869,6 @@ def create_fabricate_subroutine(self) -> func.FuncOp: magic_state_qubits[state_prep_index] = t_op.results[0] # Perform unitary encoding circuit for the code - - # def cnot_fn(ctrl_idx, trgt_ids): - # cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) - # magic_state_qubits[ctrl_idx] = cnot_op.results[0] - # magic_state_qubits[trgt_idx] = cnot_op.results[1] - - # # hadamards = unitary_encoding_info["hadamard_indices"] - # # cnot_pairs = unitary_encoding_info["cnot_indices"] - - # # for idx in hadamards: - # # h = qecp.HadamardOp(magic_state_qubits[idx]) - # # magic_state_qubits[idx] = h.results[0] - - # # for ctrl_idx, trgt_idx in cnot_pairs: - # # cnot_op = qecp.CnotOp(magic_state_qubits[ctrl_idx], magic_state_qubits[trgt_idx]) - # # magic_state_qubits[ctrl_idx] = cnot_op.results[0] - # # magic_state_qubits[trgt_idx] = cnot_op.results[1] - for op, wire_idxs in unitary_encoding_info["ops"]: op_out = op(*[magic_state_qubits[idx] for idx in wire_idxs]) for i, idx in enumerate(wire_idxs): From 0648f7f0c530abafeb6f54b7e0134e34e9d64360 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 9 Jun 2026 12:08:02 -0400 Subject: [PATCH 104/120] only generate needed qecl->qecp subroutines --- .../transforms/qecp/convert_qecl_to_qecp.py | 82 +++++++++++++++---- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 40 ++++++--- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index f383c09eb7..ff9a0fea4e 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -473,6 +473,22 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: n=self.qec_code.n, number_errors=self.number_errors ).apply(ctx, op) + # establish which optional subroutines will be needed for this circuit + uses_measure = False + circuit_gate_ops = set() + used_init_states = set() + + for inner_op in op.walk(): + if isinstance(inner_op, (qecl.SingleQubitLogicalGateOp, qecl.CnotOp)): + gate_name = inner_op.name.split(".")[1] + if getattr(inner_op, "adjoint", False): + gate_name += "_adj" + circuit_gate_ops.add(gate_name) + elif isinstance(inner_op, qecl.FabricateOp): + used_init_states.add(inner_op.init_state.data) + elif isinstance(inner_op, qecl.MeasureOp): + uses_measure = True + module_block = op.regions[0].blocks.first assert module_block is not None, "Module has no block" @@ -483,22 +499,26 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: qec_cycle_subroutine = self.create_qec_cycle_subroutine() module_block.add_op(qec_cycle_subroutine) - measure_subroutine = self.create_measure_subroutine() - module_block.add_op(measure_subroutine) + # Insert measurement subroutines + measure_subroutine = None + physical_meas_decode_subroutine = None - physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine() - module_block.add_op(physical_meas_decode_subroutine) + if uses_measure: + measure_subroutine = self.create_measure_subroutine() + physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine() + module_block.add_op(measure_subroutine) + module_block.add_op(physical_meas_decode_subroutine) - fabricate_subroutines = self.create_fabricate_subroutines() + # Insert subroutines for state evolution ops + fabricate_subroutines = self.create_fabricate_subroutines(used_init_states) for subroutine in fabricate_subroutines.values(): module_block.add_op(subroutine) # 1Q gate and 2Q gate subroutines are returned as dicts storing # {"gate_name": subroutine_funcop} - transversal_gate_subroutines = ( - self.create_transversal_1Qgate_subroutines() - | self.create_transversal_2Qgate_subroutines() - ) + transversal_gate_subroutines = self.create_transversal_1Qgate_subroutines( + circuit_gate_ops + ) | self.create_transversal_2Qgate_subroutines(circuit_gate_ops) for subroutine in transversal_gate_subroutines.values(): module_block.add_op(subroutine) @@ -819,16 +839,20 @@ def create_encode_subroutine(self) -> func.FuncOp: # MARK: Fabricate subroutines - def create_fabricate_subroutines(self) -> dict[str, func.FuncOp]: + def create_fabricate_subroutines(self, used_init_states: set[str]) -> dict[str, func.FuncOp]: """Create a subroutine that allocates a codeblock and encodes it in the magic state for the QEC code (based on the tanner graph), and returns the encoded codeblock. This is a non-fault tolerant encoding intended for use on a simulator, and not a distillation process for generating a magic state from many noisy copies. + Args: + used_init_states (set[str]): the init_states used in the circuit being compiled. + The function will create only the subroutines relevant to the current circuit. + The encoding process follows the third option for magic state encoding described in - https://arxiv.org/pdf/1303.4291 (Sec. II), with the modification that the correction is - SX as decribed in Nielsen & Chuang, (Section 10.6.2), rather than a single NOT gate. - This was found to produce the correct result for circuit simulations. + https://arxiv.org/pdf/1303.4291 (Sec. II), with the modification that when using it to + apply a T-gate, the correction is SX as decribed in Nielsen & Chuang, (Section 10.6.2), + rather than a single NOT gate. This produces the correct result for circuit simulations. The encoding method involves putting the initial QEC physical qubit in the desired state via application of a Hadamard and physical T gate, and then using the gate encoding for the @@ -852,7 +876,12 @@ def create_fabricate_subroutines(self) -> dict[str, func.FuncOp]: subroutines = {} - for init_state in ["magic", "magic_conj"]: + for init_state in used_init_states: + + assert init_state in [ + "magic", + "magic_conj", + ], "only magic and magic_conj are implemented for qecl.fabricate" codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) input_types = () @@ -915,9 +944,15 @@ def create_fabricate_subroutines(self) -> dict[str, func.FuncOp]: # MARK: 1Q gate subroutines - def create_transversal_1Qgate_subroutines(self) -> dict[str, func.FuncOp]: + def create_transversal_1Qgate_subroutines( + self, circuit_gate_ops: set[str] + ) -> dict[str, func.FuncOp]: """Create the subroutines that performs transversal 1-qubit gates on a physical codeblock. + Args: + circuit_gate_ops (set[str]): the gate ops found in the circuit being compiled. + The function will create only the subroutines relevant to the current circuit. + The subroutines are built based on the gate and codeblock indices defined in the specified ``QecCode``. For example, a code that specifies ``{"x": (qecp.PauliXOp, [2, 3])}`` as a transversal gate will lower ``qecl.x`` to ``qecp.x`` at indices 2 and 3. The `qecp.identity` @@ -937,6 +972,11 @@ def create_transversal_1Qgate_subroutines(self) -> dict[str, func.FuncOp]: output_types = (codeblock_type,) for gate_name, gate_info in single_qubit_gates.items(): + + if gate_name not in circuit_gate_ops: + # skip this one if its not included in the circuit ops + continue + gate_op, gate_indices = gate_info block = Block(arg_types=input_types) @@ -976,9 +1016,15 @@ def create_transversal_1Qgate_subroutines(self) -> dict[str, func.FuncOp]: # MARK: 2Q gate subroutines - def create_transversal_2Qgate_subroutines(self) -> dict[str, func.FuncOp]: + def create_transversal_2Qgate_subroutines( + self, circuit_gate_ops: set[str] + ) -> dict[str, func.FuncOp]: """Create the subroutines that perform transversal 2-qubit gates on a physical codeblock. + Args: + circuit_gate_ops (set[str]): the gate ops found in the circuit being compiled. + The function will create only the subroutines relevant to the current circuit. + This implementation assumes the gate is applied transversally between all corresponding qubit pairs in the two codeblocks. @@ -993,6 +1039,10 @@ def create_transversal_2Qgate_subroutines(self) -> dict[str, func.FuncOp]: for gate_name, gate_op in self.qec_code.transversal_2q_gates.items(): + if gate_name not in circuit_gate_ops: + # skip this one if its not included in the circuit ops + continue + block = Block(arg_types=input_types) with ImplicitBuilder(block): diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index a709f325b5..27911da7b5 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1177,10 +1177,6 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK: [[cb1:%.+]] = func.call @apply_T([[cb0]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> %2 = func.call @apply_T(%0) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> - - // CHECK: [[cb2:%.+]] = func.call @apply_T_adj([[cb1]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> - %3 = func.call @apply_T_adj(%2) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> - return } // CHECK-LABEL: func.func private @apply_T([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) // CHECK: func.call @fabricate_magic_Steane() : () -> !qecp.codeblock<1 x 7> @@ -1190,13 +1186,6 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): qecl.dealloc_cb %0 : !qecl.codeblock<1> func.return %1 : !qecl.codeblock<1> } - // CHECK-LABEL: func.func private @apply_T_adj([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) - // CHECK: func.call @fabricate_magic_conj_Steane() : () -> !qecp.codeblock<1 x 7> - func.func private @apply_T_adj(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { - %1 = qecl.fabricate[magic_conj] : !qecl.codeblock<1> - qecl.dealloc_cb %0 : !qecl.codeblock<1> - func.return %1 : !qecl.codeblock<1> - } // CHECK-LABEL: func.func private @fabricate_magic_Steane // CHECK: qecp.alloc_cb // CHECK: qecp.h @@ -1204,10 +1193,39 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK-NOT: qecp.t [[qb:%.+]] adj // CHECK: qecp.h // CHECK: qecp.cnot + // CHECK-NOT: func.func private @fabricate_magic_conj_Steane() + } + """ + run_filecheck(program, qecl_to_qecp_steane_pipeline) + + def test_apply_adj_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): + """Test that the call signature for the apply_T_adj subroutine is updated as expected.""" + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 7> + %0 = "test.op"() : () -> !qecl.codeblock<1> + + // CHECK: [[cb1:%.+]] = func.call @apply_T_adj([[cb0]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> + %1 = func.call @apply_T_adj(%0) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + return + } + // CHECK-LABEL: func.func private @apply_T_adj([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) + // CHECK: func.call @fabricate_magic_conj_Steane() : () -> !qecp.codeblock<1 x 7> + func.func private @apply_T_adj(%0: !qecl.codeblock<1>) -> !qecl.codeblock<1> { + %1 = qecl.fabricate[magic_conj] : !qecl.codeblock<1> + qecl.dealloc_cb %0 : !qecl.codeblock<1> + func.return %1 : !qecl.codeblock<1> + } + // CHECK-NOT: func.func private @fabricate_magic_Steane // CHECK-LABEL: func.func private @fabricate_magic_conj_Steane // CHECK: qecp.alloc_cb + // CHECK: qecp.h // CHECK: qecp.t [[qb:%.+]] adj // CHECK-NOT: qecp.t [[qb:%.+]] : + // CHECK: qecp.h + // CHECK: qecp.cnot } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) From 67dbcb8ac758b220002fde59fdf7f6508b056fb8 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 9 Jun 2026 13:36:52 -0400 Subject: [PATCH 105/120] test edits --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index a5f7e6e57b..8e91386744 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1314,17 +1314,17 @@ def circuit(): run_filecheck_qjit(circuit) -# MARK: Extensibility +# MARK: Generality -class TestExtensibility(): - """Test the extensibility to other k=1 CSS codes by testing compilation with the Shor-913 code. - Note that this code does not support any transversal phase gates. These tests check - lowering to the qecp dialect, rather than execution and validity of results.""" +class TestGenerality(): + """Test the generality for other k=1 CSS codes beyond the Steane code by testing compilation + with the Shor-913 code. Note that this code does not support any transversal phase gates. These + tests check lowering to the qecp dialect, rather than execution and validity of results.""" def test_transversal_gates(self): """ToDo: docstring. Note we add no noise.""" - dev = qp.device("lightning.qubit", wires=1) + dev = qp.device("lightning.qubit", wires=2) pipe = [("pipe", ["quantum-compilation-stage"])] @qp.qjit(capture=True, pipelines=pipe, target="mlir") @@ -1351,11 +1351,11 @@ def test_fabricate_magic_state_shor(self, run_filecheck): program = """ builtin.module @module_circuit { func.func @test_func() attributes {quantum.node} { - // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_state_Shor913() : () -> !qecp.codeblock<1 x 9> + // CHECK: [[magic_cb:%.+]] = func.call @fabricate_magic_Shor913() : () -> !qecp.codeblock<1 x 9> %0 = qecl.fabricate[magic] : !qecl.codeblock<1> return } - // CHECK-LABEL: func.func private @fabricate_magic_state_Shor913() -> !qecp.codeblock<1 x 9> + // CHECK-LABEL: func.func private @fabricate_magic_Shor913() -> !qecp.codeblock<1 x 9> // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 9> // CHECK-DAG: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit // CHECK-DAG: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit From f1ad3e3d82a3a18753d7a33a6f6fea83f0688956 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Tue, 9 Jun 2026 13:37:19 -0400 Subject: [PATCH 106/120] fix false assumption about tanner graphs --- .../transforms/qecp/convert_qecl_to_qecp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 5c21e022af..f6e28d21b8 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -1249,7 +1249,14 @@ def _qec_cycle_css_pattern( block. """ # Allocate auxiliary qubits for ESM checks - aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.x_tanner) + match check_type: + case CheckType.X: + aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.x_tanner) + case CheckType.Z: + aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.z_tanner) + case _: + assert False, f"Unknown CheckType: '{check_type}'" + aux_qubits = [ cast(OpResult[qecp.QecPhysicalQubitType], op.results[0]) for op in aux_allocate_ops ] From da083b234ff96bd2310b8531b69a0b24e9ec518e Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 10 Jun 2026 13:53:05 -0400 Subject: [PATCH 107/120] add references, fix errors --- .../transforms/qecp/qec_code_lib.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index ae95d0ba6c..e310a08ade 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -29,8 +29,10 @@ 7, 1, 3, + #### Stabilizers #### np.array([[1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 0, 1, 1]]), np.array([[1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 0, 1, 1]]), + #### Transversal gates #### { # keys need to match the names of the corresponding qecl.gate gates; if any adjoint # gates are supported, they should be included as a separate entry with key "gatename_adj" @@ -46,10 +48,11 @@ { "cnot": qecp.CnotOp, }, + #### Unitary encoding circuit #### { # ops (in the form of a qecp operator and the indices of the codeblock - # it should be applied on) defining a transporter encoding circuit, i.e. - # one that maps an input to the logical version of that input, rather + # it should be applied on) defining a transporter encoding circuit, i.e. + # one that maps an input to the logical version of that input, rather # than just encoding logical 0 "ops": [ (qecp.HadamardOp, [1]), @@ -74,26 +77,33 @@ "state_prep_index": 6, }, ), - # add ref "Shor913": ( + # see Steane code for general comments on the inputs to define the code 9, 1, 3, + #### Stabilizers #### + # from error correction zoo, https://errorcorrectionzoo.org/c/shor_nine np.array([[1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1]]), np.array( [ [1, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 2, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1], ] ), - # Z is applied transversally using physial X, and vice versa. - # There are no transversal phase gates for this code. - {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 3, 6])}, + #### Transversal gates #### + # X: physical Z on a single qubit from each set of 3, make +|111> into -|111> and vice-versa + # Z: X-flip on the all the bits on one set of 3: doesn't modify |0>, generates overall -1 sign for |1> + # CNOT is transversal for all CSS codes + # There are no Hadamard or S gates for this code #ToDo: reference + {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, {"cnot": qecp.CnotOp}, + #### Unitary encoding circuit #### + # jukebox generated this and verified it in simulation. # ToDo: provide validation info on notion { "ops": [ (qecp.CnotOp, [0, 3]), @@ -108,9 +118,6 @@ (qecp.CnotOp, [6, 7]), (qecp.CnotOp, [6, 8]), ], - # The state_prep_index is the index of the physical qubit that the state is - # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). - # Add reference "state_prep_index": 0, }, ), @@ -137,12 +144,14 @@ class QecCode: op to be applied, and the indices. Assumes k=1. Does not specify indices - for now, we assume 2-qubit gates between two codeblocks, where the gate is applied between all pairs of corresponding qubits. - unitary_encoding (dict): A dictionary defining the unitary encoding for the - ground state, including indices in the code block to prepare the qubits in the |+> - state by applying a Hadamard, and indices to apply CNOT gates. Also included is a - state-prep index. This is the index to apply physical gates to before encoding - to encode a non-zero state - for example, applying H-T at this index before unitary - encoding generates a magic state (not fault-tolerantly). + unitary_encoding (dict): A dictionary defining the unitary encoding for the code words. + It includes 'ops' (a list of tuples that each indicate a qecp gate and the codeblock + indices it should be applied to), and a state-prep index. The state-prep index is the + index to apply physical gates to before encoding, in order to encode a non-zero state + - for example, applying H-T at this index before unitary encoding generates a magic + state (not fault-tolerantly). For this to work, the chosen encoder should be an isometric + encoder, i.e. it should map the input on one of the wires to the codespace, rather than + just encoding zero. """ name: str From 49d6a260cd617ed5bbc2dea18fe16f1efa603ce8 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 10 Jun 2026 13:53:36 -0400 Subject: [PATCH 108/120] black formatting --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index f6e28d21b8..567de99dbf 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -1256,7 +1256,7 @@ def _qec_cycle_css_pattern( aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.z_tanner) case _: assert False, f"Unknown CheckType: '{check_type}'" - + aux_qubits = [ cast(OpResult[qecp.QecPhysicalQubitType], op.results[0]) for op in aux_allocate_ops ] From b4df44aede45c63699126618b2744f89c18ebad4 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 10 Jun 2026 15:54:19 -0400 Subject: [PATCH 109/120] add more comments/info --- .../python_interface/transforms/qecp/qec_code_lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index e310a08ade..d7210ec369 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -99,11 +99,13 @@ # X: physical Z on a single qubit from each set of 3, make +|111> into -|111> and vice-versa # Z: X-flip on the all the bits on one set of 3: doesn't modify |0>, generates overall -1 sign for |1> # CNOT is transversal for all CSS codes - # There are no Hadamard or S gates for this code #ToDo: reference + # There are no transversal Hadamard or S gates for this code {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, {"cnot": qecp.CnotOp}, #### Unitary encoding circuit #### - # jukebox generated this and verified it in simulation. # ToDo: provide validation info on notion + # jukebox generated this and verified it in simulation + # see simulation to validate here: + # https://app.notion.com/p/xanaduai/Shor-9-1-3-code-37bbc6bd17648058841aed64f771ee9c { "ops": [ (qecp.CnotOp, [0, 3]), From b696e20cd4b730a38daf6a8a373e5992e7718cfb Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 10 Jun 2026 16:20:00 -0400 Subject: [PATCH 110/120] add remaining tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 236 +++++++++++++++++- 1 file changed, 225 insertions(+), 11 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 8e91386744..d1e876dd1b 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1316,13 +1316,15 @@ def circuit(): # MARK: Generality -class TestGenerality(): - """Test the generality for other k=1 CSS codes beyond the Steane code by testing compilation + +class TestGenerality: + """Test the generality for other k=1 CSS codes beyond the Steane code by testing compilation with the Shor-913 code. Note that this code does not support any transversal phase gates. These tests check lowering to the qecp dialect, rather than execution and validity of results.""" - def test_transversal_gates(self): - """ToDo: docstring. Note we add no noise.""" + def test_transversal_gates(self, run_filecheck_qjit): + """Test that compilation for the code runs as expected without raising any errors + from the frontend through to the qecp layer.""" dev = qp.device("lightning.qubit", wires=2) pipe = [("pipe", ["quantum-compilation-stage"])] @@ -1333,20 +1335,233 @@ def test_transversal_gates(self): @qp.set_shots(1000) @qp.qnode(dev, mcm_method="one-shot") def circ(): + # CHECK: func.call @qec_cycle_Shor913 + # CHECK: func.call @x_Shor913 + # CHECK: func.call @z_Shor913 + # CHHECK: func.call @cnot_Shor913 + # CHECK: func.call @measure_transversal_Shor913 qp.X(0) qp.Z(1) qp.CNOT([0, 1]) m0 = qp.measure(0) m1 = qp.measure(1) return qp.sample([m0, m1]) - - mlir = circ.mlir_opt - print(mlir) - raise RuntimeError + + run_filecheck_qjit(circ) + + def test_x_shor(self, run_filecheck): + """Test that using the Shor913 code lowers PauliX expected. A PauliZ is applied to the first qubit of + each set of 3 in the nine-qubit code.""" + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK-NEXT: [[codeblock2:%.+]] = func.call @x_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK-NOT: qecl.x + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.x %0[0] : !qecl.codeblock<1> + return + } + // CHECK: func.func private @x_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.z [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.identity [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.identity [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.z [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.z [[q6]] : !qecp.qubit + // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit + // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit + // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: func.return [[codeblock_out]] + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) + + def test_z_shor(self, run_filecheck): + """Test that using the Shor913 code lowers PauliZ expected. A PauliX is applied to all the qubits in + the first set of 3 in the nine-qubit code.""" + + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK-NEXT: [[codeblock2:%.+]] = func.call @z_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK-NOT: qecl.z + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.z %0[0] : !qecl.codeblock<1> + return + } + // CHECK: func.func private @z_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.x [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.x [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.x [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.identity [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.identity [[q6]] : !qecp.qubit + // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit + // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit + // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: func.return [[codeblock_out]] + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) + + def test_qec_cycle_shor(self, run_filecheck): + """Test that a `qecl.qec` op is lowered to a call to the QEC-cycle subroutine for the Shor913 + code. + """ + program = """ + // CHECK-LABEL: test_module + builtin.module @test_module { + // CHECK-LABEL: test_program + func.func @test_program() { + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + %0 = "test.op"() : () -> !qecl.codeblock<1> + + // CHECK: [[cb1:%.+]] = func.call @qec_cycle_Shor913([[cb0]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + %1 = qecl.qec %0 : !qecl.codeblock<1> + return + } + // CHECK-LABEL: qec_cycle_Shor913([[cb0:%.+]]: !qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK: [[tanner_x:%.+]] = qecp.assemble_tanner {{.+}}, {{.+}} : tensor<24xi32>, tensor<12xi32> -> !qecp.tanner_graph<24, 12, i32> + // CHECK: [[tanner_z:%.+]] = qecp.assemble_tanner {{.+}}, {{.+}} : tensor<24xi32>, tensor<16xi32> -> !qecp.tanner_graph<24, 16, i32> + + // COM: The block below takes results of X checks and performs Z corrections + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.hadamard {{.*}} : !qecp.qubit + // CHECK: qecp.hadamard {{.*}} : !qecp.qubit + // CHECK: qecp.extract {{.*}} : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: qecp.cnot {{.*}} : !qecp.qubit, !qecp.qubit + // CHECK: qecp.insert {{.*}} : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0:%.+]] = qecp.insert {{.*}}[6], {{.*}} : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: qecp.hadamard {{.*}} : !qecp.qubit + // CHECK: qecp.hadamard {{.*}} : !qecp.qubit + // CHECK: [[m0:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m1:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: [[esm:%.+]] = tensor.from_elements [[m0]], [[m1]] : tensor<2xi1> + // CHECK: [[idx_t:%.+]] = qecp.decode_esm_css([[tanner_x]] : !qecp.tanner_graph<24, 12, i32>) [[esm]] : tensor<2xi1> -> tensor<1xindex> + // CHECK: [[lb:%.+]] = arith.constant 0 : index + // CHECK: [[ub:%.+]] = arith.constant 1 : index + // CHECK: [[st:%.+]] = arith.constant 1 : index + // CHECK: [[cb_x_out:%.+]] = scf.for [[i:%.+]] = [[lb]] to [[ub]] step [[st]] iter_args([[cb_arg:%.+]] = {{%.+}}) + // CHECK: [[err_idx:%.+]] = tensor.extract [[idx_t]][[[i]]] : tensor<1xindex> + // CHECK: [[err_i64:%.+]] = arith.index_cast [[err_idx]] : index to i64 + // CHECK: [[minus1:%.+]] = arith.constant -1 : i64 + // CHECK: [[cond:%.+]] = arith.cmpi ne, [[err_i64]], [[minus1]] : i64 + // CHECK: [[cond_out_cb:%.+]] = scf.if [[cond]] + // CHECK: [[q0:%.+]] = qecp.extract [[cb_arg]][[[err_idx]]] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.z [[q0]] : !qecp.qubit + // CHECK: [[cb_arg_1:%.+]] = qecp.insert [[cb_arg]][[[err_idx]]], [[q1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: scf.yield [[cb_arg_1]] : !qecp.codeblock<1 x 9> + // CHECK: } else { + // CHECK: scf.yield [[cb_arg]] : !qecp.codeblock<1 x 9> + // CHECK: } + // CHECK: scf.yield [[cond_out_cb]] : !qecp.codeblock<1 x 9> + // CHECK: } + + // COM: The block below takes results of X checks and performs Z corrections + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK: qecp.alloc_aux : !qecp.qubit + // CHECK-NOT: qecp.hadamard + // CHECK: qecp.extract {{.*}} : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: qecp.cnot {{.*}} : !qecp.qubit, !qecp.qubit + // CHECK: qecp.insert {{.*}} : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0:%.+]] = qecp.insert {{.*}}[6], {{.*}} : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NOT: qecp.hadamard + // CHECK: [[m0:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m1:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m2:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m3:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m4:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: [[m5:%.+]], {{.*}} = qecp.measure {{.*}} : i1, !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: qecp.dealloc_aux {{.*}} : !qecp.qubit + // CHECK: [[esm:%.+]] = tensor.from_elements [[m0]], [[m1]], [[m2]], [[m3]], [[m4]], [[m5]] : tensor<6xi1> + // CHECK: [[idx_t:%.+]] = qecp.decode_esm_css([[tanner_z]] : !qecp.tanner_graph<24, 16, i32>) [[esm]] : tensor<6xi1> -> tensor<1xindex> + // CHECK: [[lb:%.+]] = arith.constant 0 : index + // CHECK: [[ub:%.+]] = arith.constant 1 : index + // CHECK: [[st:%.+]] = arith.constant 1 : index + // CHECK: [[cb_x_out:%.+]] = scf.for [[i:%.+]] = [[lb]] to [[ub]] step [[st]] iter_args([[cb_arg:%.+]] = {{%.+}}) + // CHECK: [[err_idx:%.+]] = tensor.extract [[idx_t]][[[i]]] : tensor<1xindex> + // CHECK: [[err_i64:%.+]] = arith.index_cast [[err_idx]] : index to i64 + // CHECK: [[minus1:%.+]] = arith.constant -1 : i64 + // CHECK: [[cond:%.+]] = arith.cmpi ne, [[err_i64]], [[minus1]] : i64 + // CHECK: [[cond_out_cb:%.+]] = scf.if [[cond]] + // CHECK: [[q0:%.+]] = qecp.extract [[cb_arg]][[[err_idx]]] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.x [[q0]] : !qecp.qubit + // CHECK: [[cb_arg_1:%.+]] = qecp.insert [[cb_arg]][[[err_idx]]], [[q1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: scf.yield [[cb_arg_1]] : !qecp.codeblock<1 x 9> + // CHECK: } else { + // CHECK: scf.yield [[cb_arg]] : !qecp.codeblock<1 x 9> + // CHECK: } + // CHECK: scf.yield [[cond_out_cb]] : !qecp.codeblock<1 x 9> + // CHECK: } + // CHECK: func.return [[cb_x_out]] : !qecp.codeblock<1 x 9> + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) def test_fabricate_magic_state_shor(self, run_filecheck): - """Todo. Note that this has to be a lit test for fabricate op, because without transversal - S, we can't run the apply_T subroutine""" + """Test that the `fabricate` op for the magic state is generated as expected. Note that + without transversal S, we can't lower the apply_T subroutine, so we only test the + generation of the `fabricate` subroutine. + + Since this is only used in applying T at the moment, this isn't reachable from any + frontend code, but we can still check that it works.""" program = """ builtin.module @module_circuit { @@ -1388,4 +1603,3 @@ def test_fabricate_magic_state_shor(self, run_filecheck): """ pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) run_filecheck(program, pipeline) - From 35c0baae435af451f37e1ec1ac8a6796bef4d38a Mon Sep 17 00:00:00 2001 From: lillian542 Date: Wed, 10 Jun 2026 16:21:46 -0400 Subject: [PATCH 111/120] black formatting --- .../python_interface/transforms/qecp/qec_code_lib.py | 4 ++-- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index d7210ec369..4b4f461eae 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -103,8 +103,8 @@ {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, {"cnot": qecp.CnotOp}, #### Unitary encoding circuit #### - # jukebox generated this and verified it in simulation - # see simulation to validate here: + # jukebox generated this and verified it in simulation + # see simulation to validate here: # https://app.notion.com/p/xanaduai/Shor-9-1-3-code-37bbc6bd17648058841aed64f771ee9c { "ops": [ diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index d1e876dd1b..8a1625034e 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1448,8 +1448,8 @@ def test_z_shor(self, run_filecheck): run_filecheck(program, pipeline) def test_qec_cycle_shor(self, run_filecheck): - """Test that a `qecl.qec` op is lowered to a call to the QEC-cycle subroutine for the Shor913 - code. + """Test that a `qecl.qec` op is lowered to a call to the QEC-cycle subroutine for the + Shor913 code. """ program = """ // CHECK-LABEL: test_module @@ -1556,9 +1556,9 @@ def test_qec_cycle_shor(self, run_filecheck): run_filecheck(program, pipeline) def test_fabricate_magic_state_shor(self, run_filecheck): - """Test that the `fabricate` op for the magic state is generated as expected. Note that - without transversal S, we can't lower the apply_T subroutine, so we only test the - generation of the `fabricate` subroutine. + """Test that the `fabricate` op for the magic state is generated as expected for the + Shor913 code. Note that without transversal S, we can't lower the apply_T subroutine, + so we can only test the generation of the `fabricate` subroutine. Since this is only used in applying T at the moment, this isn't reachable from any frontend code, but we can still check that it works.""" From 5f8dc107fbd974b699022204fda415c0564e633b Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Fri, 19 Jun 2026 16:03:48 -0400 Subject: [PATCH 112/120] Add missing `return` in test --- .../transforms/qecp/test_xdsl_convert_qecl_to_qecp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index c3af8ed65c..19ae7e8a55 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1225,6 +1225,7 @@ def test_apply_t_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK: [[cb1:%.+]] = func.call @apply_T([[cb0]]) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> %2 = func.call @apply_T(%0) : (!qecl.codeblock<1>) -> !qecl.codeblock<1> + return } // CHECK-LABEL: func.func private @apply_T([[in_codeblock:%.+]]: !qecp.codeblock<1 x 7>) // CHECK: func.call @fabricate_magic_Steane() : () -> !qecp.codeblock<1 x 7> From d587ddfb9b0fba68e10dfe14ae0b9d390169c300 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Fri, 19 Jun 2026 16:06:14 -0400 Subject: [PATCH 113/120] Remove duplicated test (Probably from an unresolved merge conflict...) --- .../qecp/test_xdsl_convert_qecp_to_quantum.py | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 30ff6e30b0..8e57d9ed89 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -1356,46 +1356,3 @@ def circ(): # could have a lower atol with more shots, but given test duration, not worth it assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) - @pytest.mark.parametrize( - "n, diagonalizing_gates, expected_res, shots", - [ - (1, [qp.H], 0.707, 1000), - (1, [qp.Z, qp.S, qp.H], -0.707, 1000), - # with 2 adj-T gates, expval(Y) is -1 for every shot, so we can use fewer shots - (2, [qp.Z, qp.S, qp.H], -1, 20), - ], - ) - def test_T_adj_gate_integration( - self, n, diagonalizing_gates, expected_res, shots, run_filecheck_qjit - ): - """Integration test for adjoint(T) gates.""" - - dev = qp.device("lightning.qubit", wires=1) - - @qp.qjit(capture=True, pipelines=qec_pipeline(), seed=6) - @convert_qecp_to_quantum_pass - @convert_qecl_to_qecp_pass(qec_code="Steane", number_errors=1) - @inject_noise_to_qecl_pass - @convert_quantum_to_qecl_pass(k=1) - @qp.set_shots(shots) - @qp.qnode(dev, mcm_method="one-shot") - def circ(): - # CHECK: quantum.alloc - # CHECK: func.call @apply_T_adj - # CHECK: fabricate_magic_conj_Steane - # CHECK: qecp.assemble_tanner - # CHECK: qecp.decode_esm_css - # CHECK: quantum.custom "Hadamard" - qp.Hadamard(0) - for _ in range(n): - qp.adjoint(qp.T(0)) - for op in diagonalizing_gates: - op(0) - m0 = qp.measure(0) - return qp.sample(m0) - - run_filecheck_qjit(circ) - samples = circ() - eigenvalues = [-1 if s else 1 for s in samples] - # could have a lower atol with more shots, but given test duration, not worth it - assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) From c272d6fe782683ce07384176e2b4b011e0242272 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Fri, 19 Jun 2026 16:08:55 -0400 Subject: [PATCH 114/120] Remove extra trailing line at end of file --- .../transforms/qecp/test_xdsl_convert_qecp_to_quantum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py index 8e57d9ed89..95aaf0b799 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecp_to_quantum.py @@ -1355,4 +1355,3 @@ def circ(): eigenvalues = [-1 if s else 1 for s in samples] # could have a lower atol with more shots, but given test duration, not worth it assert np.isclose(np.mean(eigenvalues), expected_res, atol=0.07) - From e32166f64f3c9df3e05c37f69f195d02dd953824 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Wed, 24 Jun 2026 16:46:52 -0400 Subject: [PATCH 115/120] Overhaul transversal measurements protocols to support other QEC codes --- .../transforms/qecp/convert_qecl_to_qecp.py | 245 +++++++------ .../transforms/qecp/qec_code_lib.py | 201 ++++++---- .../transforms/qecp/test_qec_code_lib.py | 139 ++++++- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 347 ++++++++++-------- 4 files changed, 580 insertions(+), 352 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index 50feb12d55..d651d1e68f 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -45,8 +45,18 @@ import numpy as np from xdsl.builder import ImplicitBuilder from xdsl.context import Context -from xdsl.dialects import arith, builtin, func, scf, tensor -from xdsl.dialects.builtin import I1, IndexType, SymbolRefAttr, TensorType, i1, i32, i64 +from xdsl.dialects import arith, builtin, func, linalg, scf, tensor +from xdsl.dialects.builtin import ( + I1, + ArrayAttr, + DenseArrayBase, + IndexType, + SymbolRefAttr, + TensorType, + i1, + i32, + i64, +) from xdsl.ir import Block, BlockArgument, OpResult, Region from xdsl.passes import ModulePass from xdsl.pattern_rewriter import ( @@ -61,6 +71,7 @@ from catalyst.python_interface.dialects import qecl, qecp from catalyst.python_interface.pass_api.compiler_transform import compiler_transform +from catalyst.python_interface.transforms.qecp.qec_code_lib import qecp_gate_op_from_string from catalyst.python_interface.transforms.qecp.tanner_graph_lib import ( parity_check_matrix_to_tanner_csc, ) @@ -315,7 +326,6 @@ def match_and_rewrite(self, op: qecl.MeasureOp, rewriter: PatternRewriter): """Rewrite pattern for `qecl.measure` ops.""" k = op.out_codeblock.type.k.value.data - n = self.qec_code.n # The type-converter should already raise a CompileError if the values of k don't agree; # assert just in case. @@ -337,12 +347,12 @@ def match_and_rewrite(self, op: qecl.MeasureOp, rewriter: PatternRewriter): measure_call_op := func.CallOp( callee=SymbolRefAttr(self.measure_subroutine.sym_name), arguments=(op.in_codeblock,), - return_types=(builtin.TensorType(i1, shape=(n,)), op.in_codeblock.type), + return_types=self.measure_subroutine.function_type.outputs.data, ), decode_call_op := func.CallOp( callee=SymbolRefAttr(self.physical_meas_decode_subroutine.sym_name), arguments=(measure_call_op.results[0],), - return_types=(builtin.TensorType(i1, shape=(k,)),), + return_types=self.physical_meas_decode_subroutine.function_type.outputs.data, ), extract_idx_op := arith.ConstantOp.from_int_and_width(0, IndexType()), tensor_extract_op := tensor.ExtractOp( @@ -514,7 +524,11 @@ def apply(self, ctx: Context, op: builtin.ModuleOp) -> None: if uses_measure: measure_subroutine = self.create_measure_subroutine() - physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine() + mres_tensor_type = measure_subroutine.function_type.outputs.data[0] + assert isinstance(mres_tensor_type, TensorType) + physical_meas_decode_subroutine = self.create_physical_meas_decode_subroutine( + mres_tensor_type + ) module_block.add_op(measure_subroutine) module_block.add_op(physical_meas_decode_subroutine) @@ -640,7 +654,14 @@ def insert_tanner_graph_ops_into_block( def create_measure_subroutine(self) -> func.FuncOp: """Create the subroutine that performs the transversal measurement of a physical codeblock - in the computational basis. All qubits in the codeblock are measured directly. + in the computational basis. + + The physical measurement operations are applied on the codeblock qubits as defined in the + logical Pauli Z operation of the QEC code. For instance, if the QEC defines the logical + Pauli Z operation as "ZIZIZI", codeblock qubits 0, 2, and 3 are measured, and their outcomes + are returned as a tensor<3xi1>. For logical Pauli Z operations that contain _physical_ Pauli + gates other than Z, the appropriate diagonalizing gates are applied before measuring (i.e., + H for Pauli X measurements and HS† for Pauli Y measurements). Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. @@ -648,67 +669,73 @@ def create_measure_subroutine(self) -> func.FuncOp: codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) block = Block(arg_types=(codeblock_type,)) - with ImplicitBuilder(block): - # Loop over all physical qubits in the codeblock and measure them. - in_codeblock = cast(BlockArgument[qecp.PhysicalCodeblockType], block.args[0]) - n = in_codeblock.type.n.value.data - - # Initialize an empty tensor to store the physical measurement results - empty_tensor_op = tensor.EmptyOp([], TensorType(i1, shape=(n,))) - - # Ops for lower bound, upper bound, and step size. - lb_op = arith.ConstantOp.from_int_and_width(0, IndexType()) - ub_op = arith.ConstantOp.from_int_and_width(n, IndexType()) - step_op = arith.ConstantOp.from_int_and_width(1, IndexType()) + # TODO: This should be fairly easy to adapt to other logical Pauli measurements + pauli_z_gate_ops = self.qec_code.transversal_1q_gates.get("z") - for_body = Block( - [], - arg_types=(builtin.IndexType(), empty_tensor_op.tensor.type, in_codeblock.type), - ) - - for_each_qubit_op = scf.ForOp( - lb=lb_op, - ub=ub_op, - step=step_op, - iter_args=(empty_tensor_op.tensor, in_codeblock), - body=for_body, + if pauli_z_gate_ops is None or len(pauli_z_gate_ops) == 0: + raise CompileError( + f"Failed to create transversal-measurement subroutine: the QEC code " + f"'{self.qec_code}' does not specify a logical Pauli Z operation" ) - # Build the body of the for loop. On each iteration, extract the physical qubit at the - # iteration index, measure it, and re-insert into the codeblock. Also insert the - # measurement result into the tensor. Finally, yield updated tensor of measurement - # results and the updated codeblock. - with ImplicitBuilder(for_each_qubit_op.body): - indvar = cast(BlockArgument[IndexType], for_each_qubit_op.body.block.args[0]) - mres_tensor = cast( - BlockArgument[TensorType[I1]], for_each_qubit_op.body.block.args[1] - ) - codeblock = cast( - BlockArgument[qecp.PhysicalCodeblockType], - for_each_qubit_op.body.block.args[2], - ) - - extract_op = qecp.ExtractQubitOp(codeblock=codeblock, idx=indvar) - measure_op = qecp.MeasureOp(extract_op.qubit) - insert_op = qecp.InsertQubitOp( - in_codeblock=codeblock, idx=indvar, qubit=measure_op.out_qubit - ) + assert len(pauli_z_gate_ops) == self.qec_code.n, ( + f"Invalid definition of logical Pauli Z operation: QEC code '{self.qec_code.name}' " + f"defines a physical codeblock size of n = {self.qec_code.n}, but the Pauli Z " + f"operation definition has length {len(pauli_z_gate_ops)}" + ) - tensor_insert_op = tensor.InsertOp( - measure_op.mres, dest=mres_tensor, indices=indvar - ) - scf.YieldOp(tensor_insert_op.result, insert_op.out_codeblock) + # Get the indices of the gates in the definition of the logical Pauli Z op that are not + # Identity. This gives the indices in the codeblock to be measured. + non_identity_idx = [idx for idx, op in enumerate(pauli_z_gate_ops) if op != "I"] - out_mres_tensor = for_each_qubit_op.results[0] - out_codeblock = for_each_qubit_op.results[1] - func.ReturnOp(out_mres_tensor, out_codeblock) + with ImplicitBuilder(block): + in_codeblock = cast(BlockArgument[qecp.PhysicalCodeblockType], block.args[0]) + # n = in_codeblock.type.n.value.data + + # Extract qubits at indices in codeblock where the op is not Identity + extract_ops = [qecp.ExtractQubitOp(in_codeblock, i) for i in non_identity_idx] + qubits = [ext_op.qubit for ext_op in extract_ops] + + # Insert diagonalizing gates + for i in non_identity_idx: + gate_op = pauli_z_gate_ops[i] + match gate_op: + case "Z": + pass + case "X": + hadamard_op = qecp.HadamardOp(qubits[i]) + qubits[i] = hadamard_op.out_qubit + case "Y": + hadamard_op = qecp.HadamardOp(qubits[i]) + s_adj_op = qecp.SOp(hadamard_op.out_qubit, adjoint=True) + qubits[i] = s_adj_op.out_qubit + case _: # pragma: no cover + raise CompileError( + f"Gate in logical Pauli Z definition cannot be diagonalized for " + f"measurement: '{gate_op}'. Only gates ('X', 'Y', 'Z') are supported" + ) + + # Measure and re-insert to codeblock + measure_ops = [qecp.MeasureOp(qubit) for qubit in qubits] + out_qubits = [op.out_qubit for op in measure_ops] + + codeblock = in_codeblock + for i, out_qubit in zip(non_identity_idx, out_qubits, strict=True): + insert_op = qecp.InsertQubitOp(codeblock, i, out_qubit) + codeblock = insert_op.out_codeblock + + # Pack measurement results into tensor + mres_values = [op.mres for op in measure_ops] + mres_tensor_from_elements_op = tensor.FromElementsOp(*mres_values) + + func.ReturnOp(mres_tensor_from_elements_op.result, codeblock) measure_subroutine = func.FuncOp( name=f"measure_transversal_{self.qec_code.name}", function_type=( (codeblock_type,), ( - builtin.TensorType(i1, shape=(n,)), + mres_tensor_from_elements_op.result.type, codeblock_type, ), ), @@ -720,70 +747,65 @@ def create_measure_subroutine(self) -> func.FuncOp: # MARK: Meas_decode subroutine - def create_physical_meas_decode_subroutine(self) -> func.FuncOp: + def create_physical_meas_decode_subroutine(self, in_tensor_type: TensorType) -> func.FuncOp: """Create the subroutine that performs the physical-measurement decoding of a transversal measurement in the computational basis, and returns the corresponding k logical measurement outcomes. The logical measurement outcome is determined by a parity check of the physical measurements - corresponding to the indices of the PauliZ operation - i.e. for a code where a transversal - Z gate is applied on indices [1, 2, 4], a logical Z measurement is determined by a parity - check of physical measurements at indices [1, 2, 4] of the codeblock. + corresponding to the indices of the PauliZ operation - i.e. for a code where a transversal Z + gate is applied on indices [1, 2, 4], a logical Z measurement is determined by a parity + check of physical measurements at indices [1, 2, 4] of the codeblock. Since the transversal- + measurement subroutine only returns the measurement results of the non-identity elements of + the logical Pauli Z gate, this decoding subroutine simply computes the XOR of all elements + in the input tensor of bits. Note that this method does not insert the subroutine into the module op. Instead it returns the built func.FuncOp object that can then be subsequently inserted where desired. """ - in_tensor_type = TensorType(i1, shape=(self.qec_code.n,)) out_tensor_type = TensorType(i1, shape=(self.qec_code.k,)) block = Block(arg_types=(in_tensor_type,)) - pauli_z_gate_data = self.qec_code.transversal_1q_gates.get("z") + # TODO: When we support codes with k > 1, we will need to update how we compute the + # multiple logical measurement results and how we pack them into the output tensor - err_msg = ( - f"Failed to create physical-measurement decoding subroutine: the QEC code " - f"'{self.qec_code}' does not specify a logical Pauli Z operation" - ) + with ImplicitBuilder(block): + in_phys_meas_tensor = cast(BlockArgument[TensorType[I1]], block.args[0]) - if pauli_z_gate_data is None: - raise CompileError(err_msg) + # Build a linalg.reduce op to XOR all elements in the input tensor + # of measurement results - _, pauli_z_indices = pauli_z_gate_data + # First create the destination tensor to hold the scalar result + c0 = arith.ConstantOp.from_int_and_width(0, i1) + empty_tensor_op = tensor.EmptyOp([], TensorType(i1, shape=())) + init_tensor = linalg.FillOp((c0.result,), (empty_tensor_op.tensor,)) - if len(pauli_z_indices) == 0: - raise CompileError(err_msg) + # Then build the linalg.reduce op + reduce_block = Block(arg_types=(i1, i1)) - with ImplicitBuilder(block): - in_phys_meas_tensor = cast(BlockArgument[TensorType[I1]], block.args[0]) + with ImplicitBuilder(reduce_block): + in_arg = cast(BlockArgument[I1], reduce_block.args[0]) + out_arg = cast(BlockArgument[I1], reduce_block.args[1]) - phys_meas_values: list[OpResult] = [] - for idx in pauli_z_indices: - extract_idx_op = arith.ConstantOp.from_int_and_width(idx, IndexType()) - extract_op = tensor.ExtractOp( - in_phys_meas_tensor, indices=extract_idx_op.result, result_type=i1 - ) - phys_meas_values.append(extract_op.result) - - # TODO: When we support codes with k > 1, we will need to update how we compute the - # multiple logical measurement results and how we pack them into the output tensor - - if len(phys_meas_values) == 1: - result = phys_meas_values[0] - - else: - current_xor = phys_meas_values[0] - for i in range(1, len(phys_meas_values)): - current_xor = arith.XOrIOp(current_xor, phys_meas_values[i]) - result = current_xor.result - - out_logi_meas_tensor_op = tensor.EmptyOp([], out_tensor_type) - insert_idx_op = arith.ConstantOp.from_int_and_width(0, IndexType()) - out_logi_meas_tensor_op = tensor.InsertOp( - result, - dest=out_logi_meas_tensor_op.tensor, - indices=insert_idx_op.result, + xor_op = arith.XOrIOp(in_arg, out_arg) + linalg.YieldOp(xor_op.result) + + reduce_op = linalg.ReduceOp( + input=in_phys_meas_tensor, + init=init_tensor.results[0], + dimensions=DenseArrayBase.from_list(i64, [0]), + region=Region(reduce_block), ) - func.ReturnOp(out_logi_meas_tensor_op.result) + expand_op = tensor.ExpandShapeOp( + reduce_op.result[0], + dynamic_output_shape=[], + reassociation=ArrayAttr([]), + static_output_shape=DenseArrayBase.from_list(i64, [1]), + result_type=out_tensor_type, + ) + + func.ReturnOp(expand_op.result) physical_meas_decode_subroutine = func.FuncOp( name=f"decode_physical_measurements_{self.qec_code.name}", @@ -918,7 +940,9 @@ def create_fabricate_subroutines(self, used_init_states: set[str]) -> dict[str, # Perform unitary encoding circuit for the code for op, wire_idxs in unitary_encoding_info["ops"]: - op_out = op(*[magic_state_qubits[idx] for idx in wire_idxs]) + op_out = qecp_gate_op_from_string(op)( + *[magic_state_qubits[idx] for idx in wire_idxs] + ) for i, idx in enumerate(wire_idxs): magic_state_qubits[idx] = op_out.results[i] @@ -965,19 +989,23 @@ def create_transversal_1Qgate_subroutines( single_qubit_gates = self.qec_code.transversal_1q_gates if "identity" not in single_qubit_gates: # for identity, no need to add any non-Identity gates - single_qubit_gates["identity"] = (None, []) + single_qubit_gates["identity"] = ("I",) * self.qec_code.n codeblock_type = qecp.PhysicalCodeblockType(self.qec_code.k, self.qec_code.n) input_types = (codeblock_type,) output_types = (codeblock_type,) - for gate_name, gate_info in single_qubit_gates.items(): + for gate_name, gate_ops in single_qubit_gates.items(): if gate_name not in circuit_gate_ops: # skip this one if it's not included in the circuit ops continue - gate_op, gate_indices = gate_info + assert len(gate_ops) == self.qec_code.n, ( + f"Invalid definition of transversal gate '{gate_name}': QEC code " + f"'{self.qec_code.name}' defines a physical codeblock size of " + f"n = {self.qec_code.n}, but gate definition has length {len(gate_ops)}" + ) block = Block(arg_types=input_types) @@ -989,8 +1017,8 @@ def create_transversal_1Qgate_subroutines( qubits = [ext_op.results[0] for ext_op in extract_ops] transversal_gate = [ - gate_op(qb) if idx in gate_indices else qecp.IdentityOp(qb) - for idx, qb in enumerate(qubits) + qecp_gate_op_from_string(gate_op)(qb) + for gate_op, qb in zip(gate_ops, qubits, strict=True) ] qubits_out = [op.results[0] for op in transversal_gate] @@ -1060,7 +1088,8 @@ def create_transversal_2Qgate_subroutines( # apply the gate to each pair of qubits transversal_gate = [ - gate_op(ctrl_qb, trgt_qb) for ctrl_qb, trgt_qb in zip(ctrl_qubits, trgt_qubits) + qecp_gate_op_from_string(gate_op)(ctrl_qb, trgt_qb) + for ctrl_qb, trgt_qb in zip(ctrl_qubits, trgt_qubits) ] ctrl_qbs_out = [op.results[0] for op in transversal_gate] diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 4b4f461eae..73351dffb5 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -15,13 +15,62 @@ """This module contains a library of QEC codes.""" from dataclasses import dataclass, fields +from enum import StrEnum from functools import partial -from typing import Any, Self +from typing import Any, Callable, Self import numpy as np +from xdsl.ir import Operation from catalyst.python_interface.dialects import qecp + +class SupportedGates(StrEnum): + """Enum of gate string identifiers that are supported for QEC code definition.""" + + I = "I" # Identity # noqa: E741 + X = "X" # Pauli X + Y = "Y" # Pauli Y + Z = "Z" # Pauli Z + H = "H" # Hadamard + S = "S" # S phase + Sa = "Sa" # Adjoint of S phase + CNOT = "CNOT" # CNOT + + +def qecp_gate_op_from_string(gate_str: str) -> Callable[..., Operation]: + """Parse a gate string identifier from a QEC code definition and return the corresponding + constructible qecp operation type. In cases where the gate string identifier specifies the + adjoint of a gate, a `functools.partial` wrapper object is returned with the `adjoint=True` + parameter set. + + Raises a ValueError for invalid gate string identifiers. + """ + match gate_str: + case SupportedGates.I: + return qecp.IdentityOp + case SupportedGates.X: + return qecp.PauliXOp + case SupportedGates.Y: + return qecp.PauliYOp + case SupportedGates.Z: + return qecp.PauliZOp + case SupportedGates.H: + return qecp.HadamardOp + case SupportedGates.S: + return qecp.SOp + case SupportedGates.Sa: + return partial(qecp.SOp, adjoint=True) + case SupportedGates.CNOT: + return qecp.CnotOp + case _: + supported_gates_str = ", ".join(gate for gate in SupportedGates) + raise ValueError( + f"Invalid gate in QEC code definition: '{gate_str}'. Supported gates are: " + f"{supported_gates_str}" + ) + + _CODE_REGISTRY: dict[str, tuple[Any, ...]] = { # the indices/ordering for the operators and encodings in the Steane code are those used # in https://arxiv.org/pdf/2107.07505 @@ -34,19 +83,19 @@ np.array([[1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 0, 1, 1]]), #### Transversal gates #### { - # keys need to match the names of the corresponding qecl.gate gates; if any adjoint - # gates are supported, they should be included as a separate entry with key "gatename_adj" - # values are a tuple of the qecp gate, and the indices its applied at in the codeblock - # will need to be refactored for k>1 - "x": (qecp.PauliXOp, [4, 5, 6]), - "y": (qecp.PauliYOp, [4, 5, 6]), - "z": (qecp.PauliZOp, [4, 5, 6]), - "hadamard": (qecp.HadamardOp, [0, 1, 2, 3, 4, 5, 6]), - "s": (partial(qecp.SOp, adjoint=True), [0, 1, 2, 3, 4, 5, 6]), - "s_adj": (qecp.SOp, [0, 1, 2, 3, 4, 5, 6]), + # Keys need to match the names of the corresponding qecl.gate gates; if any adjoint + # gates are supported, they should be included as a separate entry with key + # "gatename_adj". Values are a tuple of the qecp gate, and the indices its applied at in + # the codeblock will need to be refactored for k>1 + "x": ("I", "I", "I", "I", "X", "X", "X"), + "y": ("I", "I", "I", "I", "Y", "Y", "Y"), + "z": ("I", "I", "I", "I", "Z", "Z", "Z"), + "hadamard": ("H", "H", "H", "H", "H", "H", "H"), + "s": ("Sa", "Sa", "Sa", "Sa", "Sa", "Sa", "Sa"), + "s_adj": ("S", "S", "S", "S", "S", "S", "S"), }, { - "cnot": qecp.CnotOp, + "cnot": "CNOT", }, #### Unitary encoding circuit #### { @@ -55,20 +104,20 @@ # one that maps an input to the logical version of that input, rather # than just encoding logical 0 "ops": [ - (qecp.HadamardOp, [1]), - (qecp.HadamardOp, [2]), - (qecp.HadamardOp, [3]), - (qecp.CnotOp, [1, 0]), - (qecp.CnotOp, [2, 4]), - (qecp.CnotOp, [6, 5]), - (qecp.CnotOp, [2, 0]), - (qecp.CnotOp, [3, 5]), - (qecp.CnotOp, [6, 4]), - (qecp.CnotOp, [2, 6]), - (qecp.CnotOp, [3, 4]), - (qecp.CnotOp, [1, 5]), - (qecp.CnotOp, [1, 6]), - (qecp.CnotOp, [3, 0]), + ("H", [1]), + ("H", [2]), + ("H", [3]), + ("CNOT", [1, 0]), + ("CNOT", [2, 4]), + ("CNOT", [6, 5]), + ("CNOT", [2, 0]), + ("CNOT", [3, 5]), + ("CNOT", [6, 4]), + ("CNOT", [2, 6]), + ("CNOT", [3, 4]), + ("CNOT", [1, 5]), + ("CNOT", [1, 6]), + ("CNOT", [3, 0]), ], # The state_prep_index is the index of the physical qubit that the state is # injected on (i.e. for a magic state, -H-T is applied here pre-encoding). @@ -97,28 +146,34 @@ ), #### Transversal gates #### # X: physical Z on a single qubit from each set of 3, make +|111> into -|111> and vice-versa - # Z: X-flip on the all the bits on one set of 3: doesn't modify |0>, generates overall -1 sign for |1> + # Z: X-flip on the all the bits on one set of 3: doesn't modify |0>, generates overall -1 + # sign for |1> + # Y: Y = iXZ (we ignore global phase so can use Y ~ XZ) # CNOT is transversal for all CSS codes # There are no transversal Hadamard or S gates for this code - {"x": (qecp.PauliZOp, [0, 3, 6]), "z": (qecp.PauliXOp, [0, 1, 2])}, - {"cnot": qecp.CnotOp}, + { + "x": ("Z", "I", "I", "Z", "I", "I", "Z", "I", "I"), + "y": ("Y", "X", "X", "Z", "I", "I", "Z", "I", "I"), + "z": ("X", "X", "X", "I", "I", "I", "I", "I", "I"), + }, + {"cnot": "CNOT"}, #### Unitary encoding circuit #### # jukebox generated this and verified it in simulation # see simulation to validate here: # https://app.notion.com/p/xanaduai/Shor-9-1-3-code-37bbc6bd17648058841aed64f771ee9c { "ops": [ - (qecp.CnotOp, [0, 3]), - (qecp.CnotOp, [0, 6]), - (qecp.HadamardOp, [0]), - (qecp.HadamardOp, [3]), - (qecp.HadamardOp, [6]), - (qecp.CnotOp, [0, 1]), - (qecp.CnotOp, [0, 2]), - (qecp.CnotOp, [3, 4]), - (qecp.CnotOp, [3, 5]), - (qecp.CnotOp, [6, 7]), - (qecp.CnotOp, [6, 8]), + ("CNOT", [0, 3]), + ("CNOT", [0, 6]), + ("H", [0]), + ("H", [3]), + ("H", [6]), + ("CNOT", [0, 1]), + ("CNOT", [0, 2]), + ("CNOT", [3, 4]), + ("CNOT", [3, 5]), + ("CNOT", [6, 7]), + ("CNOT", [6, 8]), ], "state_prep_index": 0, }, @@ -139,19 +194,18 @@ class QecCode: x_tanner (np.ndarray): The code's X Tanner graph z_tanner (np.ndarray): The code's Z Tanner graph transversal_1q_gates (dict): A dictionary of single-qubit transversal gates. The - key should match the gate name in the qecl dialect, and the value is a tuple - containing the qecp op to be applied, and the indices. Assumes k=1. + key should match the gate name in the qecl dialect, and the value is a tuple of gate + string identifiers specifying the qecp ops to be applied. Assumes k=1. transversal_2q_gates (dict): A dictionary of two-qubit transversal gates. The - key should match the gate name in the qecl dialect, and the value is the qecp - op to be applied, and the indices. Assumes k=1. Does not specify indices - for - now, we assume 2-qubit gates between two codeblocks, where the gate is applied - between all pairs of corresponding qubits. + key should match the gate name in the qecl dialect, and the value is string identifier + of the qecp op to be applied. Assumes k=1 and that two-qubit gates are applied between + two codeblocks, where the gate is applied between all pairs of corresponding qubits. unitary_encoding (dict): A dictionary defining the unitary encoding for the code words. It includes 'ops' (a list of tuples that each indicate a qecp gate and the codeblock indices it should be applied to), and a state-prep index. The state-prep index is the - index to apply physical gates to before encoding, in order to encode a non-zero state - - for example, applying H-T at this index before unitary encoding generates a magic - state (not fault-tolerantly). For this to work, the chosen encoder should be an isometric + index to apply physical gates to before encoding, in order to encode a non-zero state - + for example, applying H-T at this index before unitary encoding generates a magic state + (not fault-tolerantly). For this to work, the chosen encoder should be an isometric encoder, i.e. it should map the input on one of the wires to the codespace, rather than just encoding zero. """ @@ -162,9 +216,9 @@ class QecCode: d: int x_tanner: np.ndarray z_tanner: np.ndarray - transversal_1q_gates: dict - transversal_2q_gates: dict - unitary_encoding: dict + transversal_1q_gates: dict[str, tuple[str, ...]] + transversal_2q_gates: dict[str, str] + unitary_encoding: dict[str, Any] def __str__(self): if self.name == "" or str.isspace(self.name): @@ -182,6 +236,29 @@ def __repr__(self): return f"QecCode(name='{name}', n={self.n}, k={self.k}, d={self.d})" + def __post_init__(self): + invalid_transversal_gates: list[str] = [] + + for gate_name, gate_ops in self.transversal_1q_gates.items(): + if len(gate_ops) != self.n: + invalid_transversal_gates.append(gate_name) + + if invalid_transversal_gates: + err_msg = ( + f"Invalid single-qubit transversal gate definition(s): attempting to instantiate a " + f"QEC code '{self.name}' with physical codeblock size n = {self.n}, but with " + f"transversal " + ) + + err_msg += ", ".join( + [ + f"gate '{gate_name}' of length {len(self.transversal_1q_gates[gate_name])}" + for gate_name in invalid_transversal_gates + ] + ) + + raise ValueError(err_msg) + @classmethod def from_dict(cls, data: dict) -> Self: """A builder function that returns a `QecCode` instance from a dictionary. @@ -192,16 +269,16 @@ def from_dict(cls, data: dict) -> Self: ------- >>> QecCode.from_dict({ - ... 'name': "Steane", - ... 'n': 7, - ... 'k': 1, - ... 'd': 3, - ... "x_tanner": np.eye(7), - ... "z_tanner": np.eye(7), - ... "transversal_1q_gates": {}, - ... "transversal_2q_gates": {}, - ... "unitary_encoding": {} - ... }) + ... 'name': "Steane", + ... 'n': 7, + ... 'k': 1, + ... 'd': 3, + ... "x_tanner": np.eye(7), + ... "z_tanner": np.eye(7), + ... "transversal_1q_gates": {}, + ... "transversal_2q_gates": {}, + ... "unitary_encoding": {} + ... }) QecCode(name='Steane', n=7, k=1, d=3) """ # Filter dictionary to keep only keys that are fields of this dataclass diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py index 96ec9bdc5c..5666d9649f 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py @@ -14,13 +14,19 @@ """Test suite for the catalyst.python_interface.transforms.qecp.qec_code_lib module.""" +import functools import re import numpy as np import pytest +from xdsl.ir import Operation from catalyst.python_interface.dialects import qecp -from catalyst.python_interface.transforms.qecp.qec_code_lib import QecCode +from catalyst.python_interface.transforms.qecp.qec_code_lib import ( + QecCode, + SupportedGates, + qecp_gate_op_from_string, +) SUPPORTED_CODES = ["Steane"] @@ -32,7 +38,11 @@ class TestQecCode: "name, n, k, d", [("Steane", 7, 1, 3), ("Shor", 9, 1, 3), ("Surface_d3", 17, 1, 3)] ) def test_constructor(self, name: str, n: int, k: int, d: int): - """Test the constructor of the `QecCode` class for various QEC codes.""" + """Test the constructor of the `QecCode` class for various QEC codes. + + Note that some parameters of the code are placeholders only and do not necessarily represent + a true QEC code. + """ qec_code = QecCode( name, n, @@ -40,8 +50,8 @@ def test_constructor(self, name: str, n: int, k: int, d: int): d, np.eye(n), np.array([1] * n), - transversal_1q_gates={"x": (qecp.PauliXOp, [0, 1, 2])}, - transversal_2q_gates={"cnot": qecp.CnotOp}, + transversal_1q_gates={"x": ("X",) * n}, + transversal_2q_gates={"cnot": "CNOT"}, unitary_encoding={ "state_prep_index": 2, "hadamard_indices": [0, 1], @@ -55,8 +65,8 @@ def test_constructor(self, name: str, n: int, k: int, d: int): assert qec_code.d == d assert np.all(qec_code.x_tanner == np.eye(n)) assert np.all(qec_code.z_tanner == np.array([1] * n)) - assert qec_code.transversal_1q_gates == {"x": (qecp.PauliXOp, [0, 1, 2])} - assert qec_code.transversal_2q_gates == {"cnot": qecp.CnotOp} + assert qec_code.transversal_1q_gates == {"x": ("X",) * n} + assert qec_code.transversal_2q_gates == {"cnot": "CNOT"} assert qec_code.unitary_encoding == { "state_prep_index": 2, "hadamard_indices": [0, 1], @@ -71,7 +81,7 @@ def test_constructor(self, name: str, n: int, k: int, d: int): ((" ", 7, 1, 3), "[[7, 1, 3]] "), ], ) - def test_str_representation(self, inputs, expected_str): + def test_str_representation(self, inputs: tuple[str, int, int, int], expected_str: str): """Test the string representation of the `QecCode` class for various inputs.""" _, n, _, _ = inputs qec_code = QecCode(*inputs, np.eye(n), np.eye(n), {}, {}, {}) @@ -87,7 +97,7 @@ def test_str_representation(self, inputs, expected_str): "d": 3, "x_tanner": np.eye(7), "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1]]), - "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, + "transversal_1q_gates": {"x": ("X",) * 7}, "transversal_2q_gates": {}, "unitary_encoding": {}, }, @@ -99,10 +109,10 @@ def test_str_representation(self, inputs, expected_str): "x_tanner": np.eye(9), "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1, 0, 1]]), "transversal_1q_gates": { - "y": (qecp.PauliYOp, [0, 1]), - "hadamdar": (qecp.HadamardOp, [2, 4]), + "y": ("Y",) * 9, + "hadamard": ("H",) * 9, }, - "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "transversal_2q_gates": {"cnot": "CNOT"}, "unitary_encoding": {}, }, { @@ -113,12 +123,11 @@ def test_str_representation(self, inputs, expected_str): "x_tanner": np.eye(7), "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1]]), "extra-field": 42, - "transversal_1q_gates": {"z": (qecp.PauliZOp, [4, 5, 6])}, - "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "transversal_1q_gates": {"z": ("Z",) * 7}, + "transversal_2q_gates": {"cnot": "CNOT"}, "unitary_encoding": { "state_prep_index": 2, - "hadamard_indices": [0, 1], - "cnot_indices": ([0, 1], [1, 2]), + "ops": [("CNOT", [0, 1]), ("H", [1])], }, }, ], @@ -135,6 +144,7 @@ def test_from_dict(self, data: dict): assert np.all(qec_code.z_tanner == data["z_tanner"]) assert qec_code.transversal_1q_gates == data["transversal_1q_gates"] assert qec_code.transversal_2q_gates == data["transversal_2q_gates"] + assert qec_code.unitary_encoding == data["unitary_encoding"] @pytest.mark.parametrize( "data", @@ -146,8 +156,8 @@ def test_from_dict(self, data: dict): "d": 3, "x_tanner": np.eye(7), "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1]]), - "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, - "transversal_2q_gates": {"cnot": qecp.CnotOp}, + "transversal_1q_gates": {"x": ("X",) * 7}, + "transversal_2q_gates": {"cnot": "CNOT"}, "unitary_encoding": {}, }, { @@ -157,12 +167,11 @@ def test_from_dict(self, data: dict): "d": 3, "x_tanner": np.eye(9), "z_tanner": np.array([[0, 0, 1, 1, 0, 1, 1, 0, 1]]), - "transversal_1q_gates": {"x": (qecp.PauliXOp, [0, 1, 2])}, + "transversal_1q_gates": {"x": ("X",) * 9}, "transversal_2q_gates": {}, "unitary_encoding": { "state_prep_index": 2, - "hadamard_indices": [0, 1], - "cnot_indices": ([0, 1], [1, 2]), + "ops": [("CNOT", [0, 1]), ("H", [1])], }, }, ], @@ -185,6 +194,7 @@ def test_constructor_with_dict_input(self, data: dict): assert np.all(qec_code.z_tanner == data["z_tanner"]) assert qec_code.transversal_1q_gates == data["transversal_1q_gates"] assert qec_code.transversal_2q_gates == data["transversal_2q_gates"] + assert qec_code.unitary_encoding == data["unitary_encoding"] @pytest.mark.parametrize("d, expected_t", [(1, 0), (2, 0), (3, 1), (4, 1), (5, 2), (6, 2)]) def test_correctable_errors_property(self, d: int, expected_t: int): @@ -217,3 +227,92 @@ def test_get_unsupported_code(self, name): """Test that the `QecCode.get()` method raises an error for supported QEC codes.""" with pytest.raises(KeyError, match=re.compile(r"QEC code .* not found")): QecCode.get(name) + + @pytest.mark.parametrize( + "transversal_1q_gates", + [ + ({"x": ()}), + ({"x": ("X",)}), + ({"x": ("X", "Z")}), + ({"x": ("X",), "z": ("Z",)}), + ({"x": ("X",), "z": ("Z", "Z")}), + ({"x": ("X", "X", "X", "X")}), + ], + ) + def test_invalid_transversal_1q_gate_definition( + self, transversal_1q_gates: dict[str, tuple[str, ...]] + ): + """Test that defining a QEC code with invalid single-qubit gate definitions raises an error. + + The definition of a single-qubit transversal gate is invalid if the number of operators + given is not equal to the size of the physical codeblock, n. + """ + n = 3 + + with pytest.raises(ValueError, match="Invalid single-qubit transversal gate definition"): + _ = QecCode( + name="TestCode", + n=n, + k=1, + d=3, + x_tanner=np.ones(n, dtype=int), + z_tanner=np.ones(n, dtype=int), + transversal_1q_gates=transversal_1q_gates, + transversal_2q_gates={"cnot": "CNOT"}, + unitary_encoding={}, + ) + + +class TestGateStringIds: + """TODO""" + + @pytest.mark.parametrize( + "gate_str, expected_op", + [ + ("I", qecp.IdentityOp), + ("X", qecp.PauliXOp), + ("Y", qecp.PauliYOp), + ("Z", qecp.PauliZOp), + ("H", qecp.HadamardOp), + ("S", qecp.SOp), + ("CNOT", qecp.CnotOp), + ], + ) + def test_qecp_gate_op_from_string_valid_standard_gates( + self, gate_str: str, expected_op: Operation + ): + """Test that all standard, non-partial valid gates return the exact operation class.""" + result = qecp_gate_op_from_string(gate_str) + assert result is expected_op + + @pytest.mark.parametrize( + "gate_str, expected_op", [("Sa", functools.partial(qecp.SOp, adjoint=True))] + ) + def test_qecp_gate_op_from_string_adjoint_gate( + self, gate_str: str, expected_op: functools.partial + ): + """Test that the 'Sa' gate returns a functools.partial object configured correctly.""" + result = qecp_gate_op_from_string(gate_str) + + assert isinstance(result, functools.partial) + assert result.func is expected_op.func + assert result.keywords == expected_op.keywords + + @pytest.mark.parametrize( + "invalid_gate", + [ + "NOT_A_GATE", + "i", # Test case sensitivity + "", # Test empty string + "CX", # Common alternative naming not in SupportedGates + ], + ) + def test_qecp_gate_op_from_string_invalid_raises_value_error(self, invalid_gate: str): + """Test that invalid gate strings raise a ValueError with a helpful message.""" + expected_msg = ( + f"Invalid gate in QEC code definition: '{invalid_gate}'. " + "Supported gates are: I, X, Y, Z, H, S, Sa, CNOT" + ) + + with pytest.raises(ValueError, match=expected_msg): + qecp_gate_op_from_string(invalid_gate) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 19ae7e8a55..ba049790c5 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -15,10 +15,12 @@ """Test module for the convert-qecl-to-qecp dialect-conversion transform.""" from functools import partial +from typing import Callable import numpy as np import pennylane as qp import pytest +from xdsl.ir import Operation from catalyst.python_interface.dialects import qecp from catalyst.python_interface.transforms.qecl import ( @@ -60,8 +62,8 @@ def _make_qec_code( n_aux: int = 3, x_tanner=None, z_tanner=None, - transversal_1q_gates=None, - transversal_2q_gates=None, + transversal_1q_gates: dict[str, tuple[str, ...]] | None = None, + transversal_2q_gates: dict[str, str] | None = None, unitary_encoding=None, ) -> QecCode: rng = np.random.default_rng(seed=42) @@ -74,23 +76,29 @@ def _make_qec_code( if transversal_1q_gates is None: transversal_1q_gates = { - "x": (qecp.PauliXOp, list(range(n))), - "y": (qecp.PauliXOp, list(range(n))), - "z": (qecp.PauliXOp, list(range(n))), - "hadamard": (qecp.HadamardOp, list(range(n))), - "s": (partial(qecp.SOp, adjoint=True), list(range(n))), + "x": ("X",) * n, + "y": ("Y",) * n, + "z": ("Z",) * n, + "hadamard": ("H",) * n, + "s": ("Sa",) * n, } if transversal_2q_gates is None: - transversal_2q_gates = {"cnot": qecp.CnotOp} + transversal_2q_gates = {"cnot": "CNOT"} if unitary_encoding is None: + hadamard_ops = tuple([("H", [i]) for i in range(n) if i % 2]) + cnot_ops = tuple([("CNOT", [i, i + 1]) for i in range(n - 1)]) + unitary_encoding = { "state_prep_index": rng.integers(n), - "hadamard_indices": [i for i in range(n) if i % 2], - "cnot_indices": [[i, i + 1] for i in range(n - 1)], + "ops": [hadamard_ops, cnot_ops], + # "hadamard_indices": [i for i in range(n) if i % 2], + # "cnot_indices": [[i, i + 1] for i in range(n - 1)], } + transversal_1q_gates + return QecCode( name=name, n=n, @@ -615,10 +623,12 @@ def test_measure_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 7> %0 = "test.op"() : () -> !qecl.codeblock<1> - // CHECK: [[mresp:%.+]], [[cb1:%.+]] = func.call @measure_transversal_Steane([[cb0]]) : ({{.*}}) -> (tensor<7xi1>, !qecp.codeblock<1 x 7>) - // CHECK: [[mresl:%.+]] = func.call @decode_physical_measurements_Steane([[mresp]]) : (tensor<7xi1>) -> tensor<1xi1> - // CHECK: [[zero:%.+]] = arith.constant 0 : index - // CHECK: [[mres0:%.+]] = tensor.extract [[mresl]][[[zero]]] : tensor<1xi1> + // CHECK: [[mresp:%.+]], [[cb1:%.+]] = func.call @measure_transversal_Steane([[cb0]]) : + // CHECK-SAME: (!qecp.codeblock<1 x 7>) -> (tensor<3xi1>, !qecp.codeblock<1 x 7>) + // CHECK: [[mresl:%.+]] = func.call @decode_physical_measurements_Steane([[mresp]]) : + // CHECK-SAME: (tensor<3xi1>) -> tensor<1xi1> + // CHECK: [[zero:%.+]] = arith.constant 0 : index + // CHECK: [[mres0:%.+]] = tensor.extract [[mresl]][[[zero]]] : tensor<1xi1> %mres0, %1 = qecl.measure %0[0] : i1, !qecl.codeblock<1> // CHECK: [[mres1:%.+]] = "test.op"([[mres0]]) : (i1) -> i1 @@ -627,38 +637,35 @@ def test_measure_steane(self, run_filecheck, qecl_to_qecp_steane_pipeline): return } // CHECK-LABEL: func.func private @measure_transversal_Steane - // CHECK-SAME: ([[cb_in:%.+]]: !qecp.codeblock<1 x 7>) -> (tensor<7xi1>, !qecp.codeblock<1 x 7>) { - // CHECK: [[mres_t:%.+]] = tensor.empty() : tensor<7xi1> - // CHECK: [[c0:%.+]] = arith.constant 0 : index - // CHECK: [[c7:%.+]] = arith.constant 7 : index - // CHECK: [[c1:%.+]] = arith.constant 1 : index - // CHECK: [[mres_t_out:%.+]], [[cb_out:%.+]] = scf.for [[idx:%.+]] = [[c0]] to [[c7]] step [[c1]] - // CHECK-SAME: iter_args([[mres_t_arg:%.+]] = [[mres_t]], [[cb_arg:%.+]] = [[cb_in]]) - // CHECK-SAME: -> (tensor<7xi1>, !qecp.codeblock<1 x 7>) { - // CHECK: [[q0:%.+]] = qecp.extract [[cb_arg]][[[idx]]] : !qecp.codeblock<1 x 7> -> !qecp.qubit - // CHECK: [[mres0:%.+]], [[q1:%.+]] = qecp.measure [[q0]] : i1, !qecp.qubit - // CHECK: [[cb2:%.+]] = qecp.insert [[cb_arg]][[[idx]]], [[q1]] : !qecp.codeblock<1 x 7>, !qecp.qubit - // CHECK: [[mres_t_1:%.+]] = tensor.insert [[mres0]] into [[mres_t_arg]][[[idx]]] : tensor<7xi1> - // CHECK: scf.yield [[mres_t_1]], [[cb2]] : tensor<7xi1>, !qecp.codeblock<1 x 7> - // CHECK: } - // CHECK: func.return [[mres_t_out]], [[cb_out]] : tensor<7xi1>, !qecp.codeblock<1 x 7> - // CHECK: } + // CHECK-SAME: ([[cb_in:%.+]]: !qecp.codeblock<1 x 7>) -> (tensor<3xi1>, !qecp.codeblock<1 x 7>) + // CHECK: [[q40:%.+]] = qecp.extract [[cb_in]][4] + // CHECK: [[q50:%.+]] = qecp.extract [[cb_in]][5] + // CHECK: [[q60:%.+]] = qecp.extract [[cb_in]][6] + // CHECK: [[m4:%.+]], [[q41:%.+]] = qecp.measure [[q40]] + // CHECK: [[m5:%.+]], [[q51:%.+]] = qecp.measure [[q50]] + // CHECK: [[m6:%.+]], [[q61:%.+]] = qecp.measure [[q60]] + // CHECK-NOT: qecp.measure + // CHECK: [[cb1:%.+]] = qecp.insert [[cb_in]][4], [[q41]] + // CHECK: [[cb2:%.+]] = qecp.insert [[cb1]][5], [[q51]] + // CHECK: [[cb3:%.+]] = qecp.insert [[cb2]][6], [[q61]] + // CHECK: [[m_3xi1:%.+]] = tensor.from_elements [[m4]], [[m5]], [[m6]] : tensor<3xi1> + // CHECK: func.return [[m_3xi1]], [[cb3]] : tensor<3xi1>, !qecp.codeblock<1 x 7> // CHECK-LABEL: func.func private @decode_physical_measurements_Steane - // CHECK-SAME: [[in_mres_t:%.+]]: tensor<7xi1>) -> tensor<1xi1> { - // CHECK: [[c4:%.+]] = arith.constant 4 : index - // CHECK: [[m4:%.+]] = tensor.extract [[in_mres_t]][[[c4]]] : tensor<7xi1> - // CHECK: [[c5:%.+]] = arith.constant 5 : index - // CHECK: [[m5:%.+]] = tensor.extract [[in_mres_t]][[[c5]]] : tensor<7xi1> - // CHECK: [[c6:%.+]] = arith.constant 6 : index - // CHECK: [[m6:%.+]] = tensor.extract [[in_mres_t]][[[c6]]] : tensor<7xi1> - // CHECK: [[xor0:%.+]] = arith.xori [[m4]], [[m5]] : i1 - // CHECK: [[xor1:%.+]] = arith.xori [[xor0]], [[m6]] : i1 - // CHECK: [[out_mres_t0:%.+]] = tensor.empty() : tensor<1xi1> - // CHECK: [[c0:%.+]] = arith.constant 0 : index - // CHECK: [[out_mres_t1:%.+]] = tensor.insert [[xor1]] into [[out_mres_t0]][[[c0]]] : tensor<1xi1> - // CHECK: func.return [[out_mres_t1]] : tensor<1xi1> - // CHECK: } + // CHECK-SAME: ([[in_mres_3xi1:%.+]]: tensor<3xi1>) -> tensor<1xi1> + // CHECK: [[c0:%.+]] = arith.constant false + // CHECK: [[empty_i1:%.+]] = tensor.empty() : tensor + // CHECK: [[init_i1:%.+]] = linalg.fill + // CHECK-SAME: ins([[c0]] : i1) outs([[empty_i1]] : tensor) -> tensor + // CHECK: [[reduced_i1:%.+]] = linalg.reduce + // CHECK-SAME: ins([[in_mres_3xi1]]:tensor<3xi1>) outs([[init_i1]]:tensor) + // CHECK-SAME: dimensions = [0] + // CHECK: ([[in:%.+]]: i1, [[out:%.+]]: i1) { + // CHECK: [[xor:%.+]] = arith.xori [[in]], [[out]] : i1 + // CHECK: linalg.yield [[xor]] : i1 + // CHECK: [[expanded_1xi1:%.+]] = tensor.expand_shape [[reduced_i1]] [] + // CHECK-SAME: output_shape [1] : tensor into tensor<1xi1> + // CHECK: func.return [[expanded_1xi1]] : tensor<1xi1> } """ run_filecheck(program, qecl_to_qecp_steane_pipeline) @@ -678,10 +685,12 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 5> %0 = "test.op"() : () -> !qecl.codeblock<1> - // CHECK: [[mresp:%.+]], [[cb1:%.+]] = func.call @measure_transversal_TestCode([[cb0]]) : ({{.*}}) -> (tensor<5xi1>, !qecp.codeblock<1 x 5>) - // CHECK: [[mresl:%.+]] = func.call @decode_physical_measurements_TestCode([[mresp]]) : (tensor<5xi1>) -> tensor<1xi1> - // CHECK: [[zero:%.+]] = arith.constant 0 : index - // CHECK: [[mres0:%.+]] = tensor.extract [[mresl]][[[zero]]] : tensor<1xi1> + // CHECK: [[mresp:%.+]], [[cb1:%.+]] = func.call @measure_transversal_TestCode([[cb0]]) : + // CHECK-SAME: (!qecp.codeblock<1 x 5>) -> (tensor<2xi1>, !qecp.codeblock<1 x 5>) + // CHECK: [[mresl:%.+]] = func.call @decode_physical_measurements_TestCode([[mresp]]) : + // CHECK-SAME: (tensor<2xi1>) -> tensor<1xi1> + // CHECK: [[zero:%.+]] = arith.constant 0 : index + // CHECK: [[mres0:%.+]] = tensor.extract [[mresl]][[[zero]]] : tensor<1xi1> %mres0, %1 = qecl.measure %0[0] : i1, !qecl.codeblock<1> // CHECK: [[mres1:%.+]] = "test.op"([[mres0]]) : (i1) -> i1 @@ -690,26 +699,39 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): return } // CHECK-LABEL: func.func private @measure_transversal_TestCode + // CHECK-SAME: ([[cb_in:%.+]]: !qecp.codeblock<1 x 5>) -> (tensor<2xi1>, !qecp.codeblock<1 x 5>) + // CHECK: [[q00:%.+]] = qecp.extract [[cb_in]][0] + // CHECK: [[q20:%.+]] = qecp.extract [[cb_in]][2] + // CHECK: [[m0:%.+]], [[q01:%.+]] = qecp.measure [[q00]] + // CHECK: [[m2:%.+]], [[q21:%.+]] = qecp.measure [[q20]] + // CHECK-NOT: qecp.measure + // CHECK: [[cb1:%.+]] = qecp.insert [[cb_in]][0], [[q01]] + // CHECK: [[cb2:%.+]] = qecp.insert [[cb1]][2], [[q21]] + // CHECK: [[m_2xi1:%.+]] = tensor.from_elements [[m0]], [[m2]] : tensor<2xi1> + // CHECK: func.return [[m_2xi1]], [[cb2]] : tensor<2xi1>, !qecp.codeblock<1 x 5> // CHECK-LABEL: func.func private @decode_physical_measurements_TestCode - // CHECK-SAME: [[in_mres_t:%.+]]: tensor<5xi1>) -> tensor<1xi1> { - // CHECK: [[c0:%.+]] = arith.constant 0 : index - // CHECK: [[m0:%.+]] = tensor.extract [[in_mres_t]][[[c0]]] : tensor<5xi1> - // CHECK: [[c2:%.+]] = arith.constant 2 : index - // CHECK: [[m2:%.+]] = tensor.extract [[in_mres_t]][[[c2]]] : tensor<5xi1> - // CHECK: [[xor0:%.+]] = arith.xori [[m0]], [[m2]] : i1 - // CHECK: [[out_mres_t0:%.+]] = tensor.empty() : tensor<1xi1> - // CHECK: [[c0:%.+]] = arith.constant 0 : index - // CHECK: [[out_mres_t1:%.+]] = tensor.insert [[xor0]] into [[out_mres_t0]][[[c0]]] : tensor<1xi1> - // CHECK: func.return [[out_mres_t1]] : tensor<1xi1> - // CHECK: } + // CHECK-SAME: ([[in_mres_2xi1:%.+]]: tensor<2xi1>) -> tensor<1xi1> + // CHECK: [[c0:%.+]] = arith.constant false + // CHECK: [[empty_i1:%.+]] = tensor.empty() : tensor + // CHECK: [[init_i1:%.+]] = linalg.fill + // CHECK-SAME: ins([[c0]] : i1) outs([[empty_i1]] : tensor) -> tensor + // CHECK: [[reduced_i1:%.+]] = linalg.reduce + // CHECK-SAME: ins([[in_mres_2xi1]]:tensor<2xi1>) outs([[init_i1]]:tensor) + // CHECK-SAME: dimensions = [0] + // CHECK: ([[in:%.+]]: i1, [[out:%.+]]: i1) { + // CHECK: [[xor:%.+]] = arith.xori [[in]], [[out]] : i1 + // CHECK: linalg.yield [[xor]] : i1 + // CHECK: [[expanded_1xi1:%.+]] = tensor.expand_shape [[reduced_i1]] [] + // CHECK-SAME: output_shape [1] : tensor into tensor<1xi1> + // CHECK: func.return [[expanded_1xi1]] : tensor<1xi1> } """ qec_code = get_generic_qec_code( n=5, k=1, d=3, - transversal_1q_gates={"z": (qecp.PauliZOp, [0, 2])}, + transversal_1q_gates={"z": ("Z", "I", "Z", "I", "I")}, ) pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) @@ -719,14 +741,13 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): "gate_data", [ (), - [("z", (qecp.PauliZOp, []))], ], ) def test_measure_with_missing_pauli_z_def_raise( self, run_filecheck, gate_data, get_generic_qec_code ): """Test that running the convert-qecl-to-qecp pass without specifying a logical Z observable - raise an error when creating the physical-measurement decoding subroutine. + raise an error when creating the transversal-measurement subroutine. """ program = """ builtin.module { @@ -750,7 +771,7 @@ def test_measure_with_missing_pauli_z_def_raise( pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) with pytest.raises( - CompileError, match="Failed to create physical-measurement decoding subroutine" + CompileError, match="Failed to create transversal-measurement subroutine" ): run_filecheck(program, pipeline) @@ -773,7 +794,7 @@ def test_no_subroutine_if_no_measure(self, get_generic_qec_code, run_filecheck): n=7, k=1, d=3, - transversal_1q_gates={"x": (qecp.PauliXOp, [])}, + transversal_1q_gates={"x": ("X",) * 7}, ) pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),) @@ -798,7 +819,7 @@ def test_single_qubit_op_lowering_generic(self, run_filecheck, get_generic_qec_c n=n, k=k, d=1, - transversal_1q_gates={"x": (qecp.PauliXOp, [0, 2]), "z": (qecp.PauliZOp, [0, 2])}, + transversal_1q_gates={"x": ("X", "I", "X"), "z": ("Z", "I", "Z")}, ) program = f""" @@ -839,8 +860,8 @@ def test_two_qubit_op_lowering_generic(self, run_filecheck, get_generic_qec_code n=n, k=k, d=1, - transversal_1q_gates={"z": (qecp.PauliZOp, [0])}, - transversal_2q_gates={"cnot": qecp.CnotOp}, + transversal_1q_gates={"z": ("Z", "I", "I")}, + transversal_2q_gates={"cnot": "CNOT"}, ) program = f""" @@ -1074,7 +1095,7 @@ def test_nontransveral_ops_ignored(self, run_filecheck, get_generic_qec_code): n=n, k=k, d=1, - transversal_1q_gates={"x": (qecp.PauliXOp, [0, 1]), "z": (qecp.PauliZOp, [0, 1])}, + transversal_1q_gates={"x": ("X", "X", "I"), "z": ("Z", "Z", "I")}, ) program = f""" @@ -1137,8 +1158,12 @@ def test_lower_fabricate_toy_code(self, init_state, adj, run_filecheck, get_gene k=1, d=1, unitary_encoding={ - "hadamard_indices": (0, 2), - "cnot_indices": ([0, 1], [2, 0]), + "ops": [ + ("H", [0]), + ("H", [2]), + ("CNOT", [0, 1]), + ("CNOT", [2, 0]), + ], "state_prep_index": 1, }, ) @@ -1506,9 +1531,7 @@ def circ(): qp.X(0) qp.Z(1) qp.CNOT([0, 1]) - m0 = qp.measure(0) - m1 = qp.measure(1) - return qp.sample([m0, m1]) + return qp.sample(wires=[0, 1]) run_filecheck_qjit(circ) @@ -1518,45 +1541,45 @@ def test_x_shor(self, run_filecheck): program = """ builtin.module @module_circuit { - func.func @test_func() attributes {quantum.node} { - // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> - // CHECK-NEXT: [[codeblock2:%.+]] = func.call @x_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> - // CHECK-NOT: qecl.x - %0 = "test.op"() : () -> !qecl.codeblock<1> - %1 = qecl.x %0[0] : !qecl.codeblock<1> - return - } - // CHECK: func.func private @x_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) - // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK: [[q0_1:%.+]] = qecp.z [[q0]] : !qecp.qubit - // CHECK: [[q1_1:%.+]] = qecp.identity [[q1]] : !qecp.qubit - // CHECK: [[q2_1:%.+]] = qecp.identity [[q2]] : !qecp.qubit - // CHECK: [[q3_1:%.+]] = qecp.z [[q3]] : !qecp.qubit - // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit - // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit - // CHECK: [[q6_1:%.+]] = qecp.z [[q6]] : !qecp.qubit - // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit - // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit - // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: func.return [[codeblock_out]] + func.func @test_func() attributes {quantum.node} { + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK-NEXT: [[codeblock2:%.+]] = func.call @x_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK-NOT: qecl.x + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.x %0[0] : !qecl.codeblock<1> + return } - """ + // CHECK: func.func private @x_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.z [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.identity [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.identity [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.z [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.z [[q6]] : !qecp.qubit + // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit + // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit + // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: func.return [[codeblock_out]] + } + """ pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) run_filecheck(program, pipeline) @@ -1567,45 +1590,45 @@ def test_z_shor(self, run_filecheck): program = """ builtin.module @module_circuit { - func.func @test_func() attributes {quantum.node} { - // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> - // CHECK-NEXT: [[codeblock2:%.+]] = func.call @z_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> - // CHECK-NOT: qecl.z - %0 = "test.op"() : () -> !qecl.codeblock<1> - %1 = qecl.z %0[0] : !qecl.codeblock<1> - return - } - // CHECK: func.func private @z_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) - // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK: [[q0_1:%.+]] = qecp.x [[q0]] : !qecp.qubit - // CHECK: [[q1_1:%.+]] = qecp.x [[q1]] : !qecp.qubit - // CHECK: [[q2_1:%.+]] = qecp.x [[q2]] : !qecp.qubit - // CHECK: [[q3_1:%.+]] = qecp.identity [[q3]] : !qecp.qubit - // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit - // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit - // CHECK: [[q6_1:%.+]] = qecp.identity [[q6]] : !qecp.qubit - // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit - // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit - // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: func.return [[codeblock_out]] + func.func @test_func() attributes {quantum.node} { + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK-NEXT: [[codeblock2:%.+]] = func.call @z_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK-NOT: qecl.z + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.z %0[0] : !qecl.codeblock<1> + return } - """ + // CHECK: func.func private @z_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.x [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.x [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.x [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.identity [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.identity [[q6]] : !qecp.qubit + // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit + // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit + // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK-NEXT: func.return [[codeblock_out]] + } + """ pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) run_filecheck(program, pipeline) @@ -1735,26 +1758,26 @@ def test_fabricate_magic_state_shor(self, run_filecheck): } // CHECK-LABEL: func.func private @fabricate_magic_Shor913() -> !qecp.codeblock<1 x 9> // CHECK: [[cb:%.+]] = qecp.alloc_cb : !qecp.codeblock<1 x 9> - // CHECK-DAG: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q7:%.+]] = qecp.extract [[cb]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-DAG: [[q8:%.+]] = qecp.extract [[cb]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // State injection on the state_prep_index (qubit 6): H then T + // CHECK-DAG: [[q0:%.+]] = qecp.extract [[cb]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q1:%.+]] = qecp.extract [[cb]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q2:%.+]] = qecp.extract [[cb]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q3:%.+]] = qecp.extract [[cb]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q4:%.+]] = qecp.extract [[cb]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q5:%.+]] = qecp.extract [[cb]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q6:%.+]] = qecp.extract [[cb]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q7:%.+]] = qecp.extract [[cb]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK-DAG: [[q8:%.+]] = qecp.extract [[cb]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // COM: State injection on the state_prep_index (qubit 6): H then T // CHECK: [[h_inj:%.+]] = qecp.hadamard [[q0]] : !qecp.qubit // CHECK: [[q0_1:%.+]] = qecp.t [[h_inj]] : !qecp.qubit - // Unitary encoding: initial CNOTs + // COM: Unitary encoding: initial CNOTs // CHECK: [[q0_2:%.+]], [[q3_1:%.+]] = qecp.cnot [[q0_1]], [[q3]] : !qecp.qubit, !qecp.qubit // CHECK: [[q0_3:%.+]], [[q6_1:%.+]] = qecp.cnot [[q0_2]], [[q6]] : !qecp.qubit, !qecp.qubit - // Unitary encoing: Hadamards on indices 0, 3, 6 + // COM: Unitary encoding: Hadamards on indices 0, 3, 6 // CHECK: [[q0_4:%.+]] = qecp.hadamard [[q0_3]] : !qecp.qubit // CHECK: [[q3_2:%.+]] = qecp.hadamard [[q3_1]] : !qecp.qubit // CHECK: [[q6_2:%.+]] = qecp.hadamard [[q6_1]] : !qecp.qubit - // Unitary encoding: more CNOTs - [n, n+1] and [n, n+2] for n in [0, 3, 6] + // COM: Unitary encoding: more CNOTs - [n, n+1] and [n, n+2] for n in [0, 3, 6] // CHECK: [[q0_5:%.+]], [[q1_1:%.+]] = qecp.cnot [[q0_4]], [[q1]] : !qecp.qubit, !qecp.qubit // CHECK: qecp.cnot [[q0_5]], [[q2]] : !qecp.qubit, !qecp.qubit // CHECK: [[q3_3:%.+]], [[q4_1:%.+]] = qecp.cnot [[q3_2]], [[q4]] : !qecp.qubit, !qecp.qubit From 050ab1bc4ac6a2f1b8459aad4580b2c90b0cbf88 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Wed, 24 Jun 2026 16:56:39 -0400 Subject: [PATCH 116/120] Placate CodeFactor --- .../transforms/qecp/qec_code_lib.py | 20 +++++++++++-------- .../transforms/qecp/test_qec_code_lib.py | 1 - .../qecp/test_xdsl_convert_qecl_to_qecp.py | 11 ++-------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py index 73351dffb5..206cd89566 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py +++ b/frontend/catalyst/python_interface/transforms/qecp/qec_code_lib.py @@ -46,23 +46,25 @@ def qecp_gate_op_from_string(gate_str: str) -> Callable[..., Operation]: Raises a ValueError for invalid gate string identifiers. """ + op_type: Callable[..., Operation] + match gate_str: case SupportedGates.I: - return qecp.IdentityOp + op_type = qecp.IdentityOp case SupportedGates.X: - return qecp.PauliXOp + op_type = qecp.PauliXOp case SupportedGates.Y: - return qecp.PauliYOp + op_type = qecp.PauliYOp case SupportedGates.Z: - return qecp.PauliZOp + op_type = qecp.PauliZOp case SupportedGates.H: - return qecp.HadamardOp + op_type = qecp.HadamardOp case SupportedGates.S: - return qecp.SOp + op_type = qecp.SOp case SupportedGates.Sa: - return partial(qecp.SOp, adjoint=True) + op_type = partial(qecp.SOp, adjoint=True) case SupportedGates.CNOT: - return qecp.CnotOp + op_type = qecp.CnotOp case _: supported_gates_str = ", ".join(gate for gate in SupportedGates) raise ValueError( @@ -70,6 +72,8 @@ def qecp_gate_op_from_string(gate_str: str) -> Callable[..., Operation]: f"{supported_gates_str}" ) + return op_type + _CODE_REGISTRY: dict[str, tuple[Any, ...]] = { # the indices/ordering for the operators and encodings in the Steane code are those used diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py index 5666d9649f..c194bbef7d 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_qec_code_lib.py @@ -24,7 +24,6 @@ from catalyst.python_interface.dialects import qecp from catalyst.python_interface.transforms.qecp.qec_code_lib import ( QecCode, - SupportedGates, qecp_gate_op_from_string, ) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index ba049790c5..80008f5db7 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -14,15 +14,10 @@ """Test module for the convert-qecl-to-qecp dialect-conversion transform.""" -from functools import partial -from typing import Callable - import numpy as np import pennylane as qp import pytest -from xdsl.ir import Operation -from catalyst.python_interface.dialects import qecp from catalyst.python_interface.transforms.qecl import ( convert_quantum_to_qecl_pass, inject_noise_to_qecl_pass, @@ -87,8 +82,8 @@ def _make_qec_code( transversal_2q_gates = {"cnot": "CNOT"} if unitary_encoding is None: - hadamard_ops = tuple([("H", [i]) for i in range(n) if i % 2]) - cnot_ops = tuple([("CNOT", [i, i + 1]) for i in range(n - 1)]) + hadamard_ops = tuple(("H", [i]) for i in range(n) if i % 2) + cnot_ops = tuple(("CNOT", [i, i + 1]) for i in range(n - 1)) unitary_encoding = { "state_prep_index": rng.integers(n), @@ -97,8 +92,6 @@ def _make_qec_code( # "cnot_indices": [[i, i + 1] for i in range(n - 1)], } - transversal_1q_gates - return QecCode( name=name, n=n, From 65e006671ae445ca4438ea82731a0a642e914c54 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 25 Jun 2026 09:37:54 -0400 Subject: [PATCH 117/120] Add 'no cover' pragmas for unreachable code --- .../python_interface/transforms/qecp/convert_qecl_to_qecp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index d651d1e68f..e8a47ee3d6 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -1292,7 +1292,7 @@ def _qec_cycle_css_pattern( aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.x_tanner) case CheckType.Z: aux_allocate_ops = (qecp.AllocAuxQubitOp() for row in self.qec_code.z_tanner) - case _: + case _: # pragma: no cover assert False, f"Unknown CheckType: '{check_type}'" aux_qubits = [ @@ -1386,7 +1386,7 @@ def _qec_cycle_css_pattern( corr_qubit_op = qecp.PauliZOp(in_qubit=err_qubit) case CheckType.Z: corr_qubit_op = qecp.PauliXOp(in_qubit=err_qubit) - case _: + case _: # pragma: no cover assert False, f"Unknown CheckType: '{check_type}'" insert_err_qubit_op = qecp.InsertQubitOp( From ec510bff5f74f51905e5593ca9769db47b0fbcc0 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 25 Jun 2026 11:39:05 -0400 Subject: [PATCH 118/120] Update qecl-to-qecp docs - Update known limitations - Update old transversal-gate def usage --- .../transforms/qecp/convert_qecl_to_qecp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index e8a47ee3d6..c174d87d0f 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -23,18 +23,16 @@ The convert-qecl-to-qecp pass has the following known limitations: * No support for QEC codes where the number of logical qubits per codeblock, k, is greater than 1. - * No support for non-CSS codes - * Only supports the transversal gates defined in the `QecCode` + T gates + * No support for non-CSS codes. + * Only supports the transversal gates defined in the `QecCode` + T gates. * Only logical Pauli Z observables (computational basis) are supported for lowering `qecl.measure` operations. - * Support for control flow in the user program is not fully implemented or tested * The generated QEC-cycles are not fault-tolerant - they don't account for potential errors in - the syndrome qubits/measurements - * For terminal measurements, only sampling of MCMs is supported + the syndrome qubits/measurements. * The encoding procedure to create the logical zero state in a codeblock is not optimized for - efficiency - * Only thoroughly tested with the Steane code, but should be generalizable to other k=1 CSS - stabilizer codes. + efficiency. + * Only thoroughly tested with the [[7, 1, 3]] Steane code, and to a lesser extent the [[9, 1, 3]] + Shor code, but it should be generalizable to other k=1 CSS stabilizer codes. """ from collections.abc import Callable, Iterable @@ -978,7 +976,7 @@ def create_transversal_1Qgate_subroutines( The function will create only the subroutines relevant to the current circuit. The subroutines are built based on the gate and codeblock indices defined in the specified - ``QecCode``. For example, a code that specifies ``{"x": (qecp.PauliXOp, [2, 3])}`` as a + ``QecCode``. For example, a code that specifies ``{"x": ("I", "I", "X", "X", "I")}`` as a transversal gate will lower ``qecl.x`` to ``qecp.x`` at indices 2 and 3. The `qecp.identity` operator is applied at all other indices in the codeblock. From 09af7143a78850946204f53ecb70ef26e58ddf40 Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 25 Jun 2026 11:39:55 -0400 Subject: [PATCH 119/120] Update and add qecl-to-qecp Shor tests --- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 286 +++++++++++++++--- 1 file changed, 239 insertions(+), 47 deletions(-) diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 80008f5db7..3af7fad0c7 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -1519,7 +1519,7 @@ def circ(): # CHECK: func.call @qec_cycle_Shor913 # CHECK: func.call @x_Shor913 # CHECK: func.call @z_Shor913 - # CHHECK: func.call @cnot_Shor913 + # CHECK: func.call @cnot_Shor913 # CHECK: func.call @measure_transversal_Shor913 qp.X(0) qp.Z(1) @@ -1529,29 +1529,33 @@ def circ(): run_filecheck_qjit(circ) def test_x_shor(self, run_filecheck): - """Test that using the Shor913 code lowers PauliX expected. A PauliZ is applied to the first qubit of - each set of 3 in the nine-qubit code.""" + """Test that using the Shor913 code lowers a logical Pauli X gate as expected. + The logical Pauli X gate in the Shor913 code is realized by applying the following Pauli + word to the physical codeblock: + + "ZIIZIIZII" + """ program = """ builtin.module @module_circuit { func.func @test_func() attributes {quantum.node} { // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> - // CHECK-NEXT: [[codeblock2:%.+]] = func.call @x_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK: [[codeblock2:%.+]] = func.call @x_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> // CHECK-NOT: qecl.x %0 = "test.op"() : () -> !qecl.codeblock<1> %1 = qecl.x %0[0] : !qecl.codeblock<1> return } - // CHECK: func.func private @x_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) - // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: func.func private @x_Shor913([[cb_0:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK: [[q0:%.+]] = qecp.extract [[cb_0]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.extract [[cb_0]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q2:%.+]] = qecp.extract [[cb_0]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q3:%.+]] = qecp.extract [[cb_0]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q4:%.+]] = qecp.extract [[cb_0]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q5:%.+]] = qecp.extract [[cb_0]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q6:%.+]] = qecp.extract [[cb_0]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q7:%.+]] = qecp.extract [[cb_0]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q8:%.+]] = qecp.extract [[cb_0]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit // CHECK: [[q0_1:%.+]] = qecp.z [[q0]] : !qecp.qubit // CHECK: [[q1_1:%.+]] = qecp.identity [[q1]] : !qecp.qubit // CHECK: [[q2_1:%.+]] = qecp.identity [[q2]] : !qecp.qubit @@ -1561,16 +1565,69 @@ def test_x_shor(self, run_filecheck): // CHECK: [[q6_1:%.+]] = qecp.z [[q6]] : !qecp.qubit // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit - // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: func.return [[codeblock_out]] + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb_0]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_4:%.+]] = qecp.insert [[cb_3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_5:%.+]] = qecp.insert [[cb_4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_6:%.+]] = qecp.insert [[cb_5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_7:%.+]] = qecp.insert [[cb_6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_8:%.+]] = qecp.insert [[cb_7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_9:%.+]] = qecp.insert [[cb_8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: return [[cb_9]] : !qecp.codeblock<1 x 9> + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) + + def test_y_shor(self, run_filecheck): + """Test that using the Shor913 code lowers a logical Pauli Y gate as expected. + + The logical Pauli Y gate in the Shor913 code is realized by applying the following Pauli + word to the physical codeblock: + + "YXXZIIZII" + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK: [[codeblock2:%.+]] = func.call @y_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK-NOT: qecl.y + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = qecl.y %0[0] : !qecl.codeblock<1> + return + } + // CHECK: func.func private @y_Shor913([[cb_0:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK: [[q0:%.+]] = qecp.extract [[cb_0]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.extract [[cb_0]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q2:%.+]] = qecp.extract [[cb_0]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q3:%.+]] = qecp.extract [[cb_0]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q4:%.+]] = qecp.extract [[cb_0]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q5:%.+]] = qecp.extract [[cb_0]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q6:%.+]] = qecp.extract [[cb_0]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q7:%.+]] = qecp.extract [[cb_0]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q8:%.+]] = qecp.extract [[cb_0]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.y [[q0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.x [[q1]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.x [[q2]] : !qecp.qubit + // CHECK: [[q3_1:%.+]] = qecp.z [[q3]] : !qecp.qubit + // CHECK: [[q4_1:%.+]] = qecp.identity [[q4]] : !qecp.qubit + // CHECK: [[q5_1:%.+]] = qecp.identity [[q5]] : !qecp.qubit + // CHECK: [[q6_1:%.+]] = qecp.z [[q6]] : !qecp.qubit + // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit + // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb_0]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_4:%.+]] = qecp.insert [[cb_3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_5:%.+]] = qecp.insert [[cb_4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_6:%.+]] = qecp.insert [[cb_5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_7:%.+]] = qecp.insert [[cb_6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_8:%.+]] = qecp.insert [[cb_7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_9:%.+]] = qecp.insert [[cb_8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: return [[cb_9]] : !qecp.codeblock<1 x 9> } """ @@ -1578,29 +1635,33 @@ def test_x_shor(self, run_filecheck): run_filecheck(program, pipeline) def test_z_shor(self, run_filecheck): - """Test that using the Shor913 code lowers PauliZ expected. A PauliX is applied to all the qubits in - the first set of 3 in the nine-qubit code.""" + """Test that using the Shor913 code lowers a logical Pauli Z gate as expected. + + The logical Pauli Z gate in the Shor913 code is realized by applying the following Pauli + word to the physical codeblock: + "XXXIIIIII" + """ program = """ builtin.module @module_circuit { func.func @test_func() attributes {quantum.node} { // CHECK: [[codeblock:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> - // CHECK-NEXT: [[codeblock2:%.+]] = func.call @z_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> + // CHECK: [[codeblock2:%.+]] = func.call @z_Shor913([[codeblock]]) : (!qecp.codeblock<1 x 9>) -> !qecp.codeblock<1 x 9> // CHECK-NOT: qecl.z %0 = "test.op"() : () -> !qecl.codeblock<1> %1 = qecl.z %0[0] : !qecl.codeblock<1> return } - // CHECK: func.func private @z_Shor913([[codeblock_in:%.+]]: !qecp.codeblock<1 x 9>) - // CHECK-NEXT: [[q0:%.+]] = qecp.extract [[codeblock_in]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q1:%.+]] = qecp.extract [[codeblock_in]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q2:%.+]] = qecp.extract [[codeblock_in]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q3:%.+]] = qecp.extract [[codeblock_in]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q4:%.+]] = qecp.extract [[codeblock_in]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q5:%.+]] = qecp.extract [[codeblock_in]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q6:%.+]] = qecp.extract [[codeblock_in]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q7:%.+]] = qecp.extract [[codeblock_in]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit - // CHECK-NEXT: [[q8:%.+]] = qecp.extract [[codeblock_in]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: func.func private @z_Shor913([[cb_0:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK: [[q0:%.+]] = qecp.extract [[cb_0]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1:%.+]] = qecp.extract [[cb_0]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q2:%.+]] = qecp.extract [[cb_0]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q3:%.+]] = qecp.extract [[cb_0]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q4:%.+]] = qecp.extract [[cb_0]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q5:%.+]] = qecp.extract [[cb_0]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q6:%.+]] = qecp.extract [[cb_0]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q7:%.+]] = qecp.extract [[cb_0]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q8:%.+]] = qecp.extract [[cb_0]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit // CHECK: [[q0_1:%.+]] = qecp.x [[q0]] : !qecp.qubit // CHECK: [[q1_1:%.+]] = qecp.x [[q1]] : !qecp.qubit // CHECK: [[q2_1:%.+]] = qecp.x [[q2]] : !qecp.qubit @@ -1610,16 +1671,88 @@ def test_z_shor(self, run_filecheck): // CHECK: [[q6_1:%.+]] = qecp.identity [[q6]] : !qecp.qubit // CHECK: [[q7_1:%.+]] = qecp.identity [[q7]] : !qecp.qubit // CHECK: [[q8_1:%.+]] = qecp.identity [[q8]] : !qecp.qubit - // CHECK-NEXT: [[codeblock_in1:%.+]] = qecp.insert [[codeblock_in]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in2:%.+]] = qecp.insert [[codeblock_in1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in3:%.+]] = qecp.insert [[codeblock_in2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in4:%.+]] = qecp.insert [[codeblock_in3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in5:%.+]] = qecp.insert [[codeblock_in4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in6:%.+]] = qecp.insert [[codeblock_in5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in7:%.+]] = qecp.insert [[codeblock_in6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_in8:%.+]] = qecp.insert [[codeblock_in7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: [[codeblock_out:%.+]] = qecp.insert [[codeblock_in8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit - // CHECK-NEXT: func.return [[codeblock_out]] + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb_0]][0], [[q0_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_4:%.+]] = qecp.insert [[cb_3]][3], [[q3_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_5:%.+]] = qecp.insert [[cb_4]][4], [[q4_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_6:%.+]] = qecp.insert [[cb_5]][5], [[q5_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_7:%.+]] = qecp.insert [[cb_6]][6], [[q6_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_8:%.+]] = qecp.insert [[cb_7]][7], [[q7_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_9:%.+]] = qecp.insert [[cb_8]][8], [[q8_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: return [[cb_9]] : !qecp.codeblock<1 x 9> + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) + + def test_cnot_shor(self, run_filecheck): + """Test that using the Shor913 code lowers a logical CNOT gate as expected. + + The logical CNOT gate in the Shor913 code is realized by transversally applying physical + CNOT gates qubit-wise between two codeblocks. + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK: [[cb1:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK: [[cb2:%.+]], [[cb3:%.+]] = func.call @cnot_Shor913([[cb0]], [[cb1]]) : + // CHECK-SAME: (!qecp.codeblock<1 x 9>, !qecp.codeblock<1 x 9>) -> (!qecp.codeblock<1 x 9>, !qecp.codeblock<1 x 9>) + // CHECK-NOT: qecl.cnot + %0 = "test.op"() : () -> !qecl.codeblock<1> + %1 = "test.op"() : () -> !qecl.codeblock<1> + %2, %3 = qecl.cnot %0[0], %1[0] : !qecl.codeblock<1>, !qecl.codeblock<1> + return + } + // CHECK: func.func private @cnot_Shor913([[cb0_0:%.+]]: !qecp.codeblock<1 x 9>, [[cb1_0:%.+]]: !qecp.codeblock<1 x 9>) + // CHECK: [[q00_0:%.+]] = qecp.extract [[cb0]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q01_0:%.+]] = qecp.extract [[cb0]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q02_0:%.+]] = qecp.extract [[cb0]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q03_0:%.+]] = qecp.extract [[cb0]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q04_0:%.+]] = qecp.extract [[cb0]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q05_0:%.+]] = qecp.extract [[cb0]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q06_0:%.+]] = qecp.extract [[cb0]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q07_0:%.+]] = qecp.extract [[cb0]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q08_0:%.+]] = qecp.extract [[cb0]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q10_0:%.+]] = qecp.extract [[cb1]][0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q11_0:%.+]] = qecp.extract [[cb1]][1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q12_0:%.+]] = qecp.extract [[cb1]][2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q13_0:%.+]] = qecp.extract [[cb1]][3] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q14_0:%.+]] = qecp.extract [[cb1]][4] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q15_0:%.+]] = qecp.extract [[cb1]][5] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q16_0:%.+]] = qecp.extract [[cb1]][6] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q17_0:%.+]] = qecp.extract [[cb1]][7] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q18_0:%.+]] = qecp.extract [[cb1]][8] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q00_1:%.+]], [[q10_1:%.+]] = qecp.cnot [[q00_0]], [[q10_0]] + // CHECK: [[q01_1:%.+]], [[q11_1:%.+]] = qecp.cnot [[q01_0]], [[q11_0]] + // CHECK: [[q02_1:%.+]], [[q12_1:%.+]] = qecp.cnot [[q02_0]], [[q12_0]] + // CHECK: [[q03_1:%.+]], [[q13_1:%.+]] = qecp.cnot [[q03_0]], [[q13_0]] + // CHECK: [[q04_1:%.+]], [[q14_1:%.+]] = qecp.cnot [[q04_0]], [[q14_0]] + // CHECK: [[q05_1:%.+]], [[q15_1:%.+]] = qecp.cnot [[q05_0]], [[q15_0]] + // CHECK: [[q06_1:%.+]], [[q16_1:%.+]] = qecp.cnot [[q06_0]], [[q16_0]] + // CHECK: [[q07_1:%.+]], [[q17_1:%.+]] = qecp.cnot [[q07_0]], [[q17_0]] + // CHECK: [[q08_1:%.+]], [[q18_1:%.+]] = qecp.cnot [[q08_0]], [[q18_0]] + // CHECK: [[cb0_1:%.+]] = qecp.insert [[cb0_0]][0], [[q00_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_2:%.+]] = qecp.insert [[cb0_1]][1], [[q01_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_3:%.+]] = qecp.insert [[cb0_2]][2], [[q02_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_4:%.+]] = qecp.insert [[cb0_3]][3], [[q03_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_5:%.+]] = qecp.insert [[cb0_4]][4], [[q04_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_6:%.+]] = qecp.insert [[cb0_5]][5], [[q05_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_7:%.+]] = qecp.insert [[cb0_6]][6], [[q06_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_8:%.+]] = qecp.insert [[cb0_7]][7], [[q07_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb0_9:%.+]] = qecp.insert [[cb0_8]][8], [[q08_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_1:%.+]] = qecp.insert [[cb1_0]][0], [[q10_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_2:%.+]] = qecp.insert [[cb1_1]][1], [[q11_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_3:%.+]] = qecp.insert [[cb1_2]][2], [[q12_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_4:%.+]] = qecp.insert [[cb1_3]][3], [[q13_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_5:%.+]] = qecp.insert [[cb1_4]][4], [[q14_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_6:%.+]] = qecp.insert [[cb1_5]][5], [[q15_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_7:%.+]] = qecp.insert [[cb1_6]][6], [[q16_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_8:%.+]] = qecp.insert [[cb1_7]][7], [[q17_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb1_9:%.+]] = qecp.insert [[cb1_8]][8], [[q18_1]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: return [[cb0_9]], [[cb1_9]] : !qecp.codeblock<1 x 9>, !qecp.codeblock<1 x 9> } """ @@ -1782,3 +1915,62 @@ def test_fabricate_magic_state_shor(self, run_filecheck): """ pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) run_filecheck(program, pipeline) + + def test_measure_shor(self, run_filecheck): + """Test that using the Shor913 code lowers a logical measurement as expected. + + Recall that a logical computational-basis measurement amounts to measuring the logical Pauli + Z observable, which in the Shor913 code is "XXXIIIIII". In order to perform a physical X + measurement, diagonalizing gates are inserted before performing the computational basis + measurement (the diagonalizing gate for X measurements is 'H'). + """ + program = """ + builtin.module @module_circuit { + func.func @test_func() attributes {quantum.node} { + // CHECK: [[cb_0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 9> + // CHECK: [[mres_3xi1:%.+]], [[cb_1:%.+]] = func.call @measure_transversal_Shor913([[cb_0]]) : + // CHECK-SAME: (!qecp.codeblock<1 x 9>) -> (tensor<3xi1>, !qecp.codeblock<1 x 9>) + // CHECK: [[mres_1xi1:%.+]] = func.call @decode_physical_measurements_Shor913([[mres_3xi1]]) : + // CHECK-SAME: (tensor<3xi1>) -> tensor<1xi1> + // CHECK-NOT: qecl.measure + %0 = "test.op"() : () -> !qecl.codeblock<1> + %mres, %1 = qecl.measure %0[0] : i1, !qecl.codeblock<1> + return + } + // CHECK-LABEL: func.func private @measure_transversal_Shor913( + // CHECK-SAME: [[cb_0]]: !qecp.codeblock<1 x 9>) -> (tensor<3xi1>, !qecp.codeblock<1 x 9>) + // CHECK: [[q0_0:%.+]] = qecp.extract %0[0] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q1_0:%.+]] = qecp.extract %0[1] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q2_0:%.+]] = qecp.extract %0[2] : !qecp.codeblock<1 x 9> -> !qecp.qubit + // CHECK: [[q0_1:%.+]] = qecp.hadamard [[q0_0]] : !qecp.qubit + // CHECK: [[q1_1:%.+]] = qecp.hadamard [[q1_0]] : !qecp.qubit + // CHECK: [[q2_1:%.+]] = qecp.hadamard [[q2_0]] : !qecp.qubit + // CHECK: [[m0:%.+]], [[q0_2:%.+]] = qecp.measure [[q0_1]] : i1, !qecp.qubit + // CHECK: [[m1:%.+]], [[q1_2:%.+]] = qecp.measure [[q1_1]] : i1, !qecp.qubit + // CHECK: [[m2:%.+]], [[q2_2:%.+]] = qecp.measure [[q2_1]] : i1, !qecp.qubit + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb_0]][0], [[q0_2]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][1], [[q1_2]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][2], [[q2_2]] : !qecp.codeblock<1 x 9>, !qecp.qubit + // CHECK: [[m_3xi1:%.+]] = tensor.from_elements [[m0]], [[m1]], [[m2]] : tensor<3xi1> + // CHECK: return [[m_3xi1]], [[cb_3]] : tensor<3xi1>, !qecp.codeblock<1 x 9> + + // CHECK-LABEL: func.func private @decode_physical_measurements_Shor913 + // CHECK-SAME: ([[in_mres_3xi1:%.+]]: tensor<3xi1>) -> tensor<1xi1> + // CHECK: [[c0:%.+]] = arith.constant false + // CHECK: [[empty_i1:%.+]] = tensor.empty() : tensor + // CHECK: [[init_i1:%.+]] = linalg.fill + // CHECK-SAME: ins([[c0]] : i1) outs([[empty_i1]] : tensor) -> tensor + // CHECK: [[reduced_i1:%.+]] = linalg.reduce + // CHECK-SAME: ins([[in_mres_3xi1]]:tensor<3xi1>) outs([[init_i1]]:tensor) + // CHECK-SAME: dimensions = [0] + // CHECK: ([[in:%.+]]: i1, [[out:%.+]]: i1) { + // CHECK: [[xor:%.+]] = arith.xori [[in]], [[out]] : i1 + // CHECK: linalg.yield [[xor]] : i1 + // CHECK: [[expanded_1xi1:%.+]] = tensor.expand_shape [[reduced_i1]] [] + // CHECK-SAME: output_shape [1] : tensor into tensor<1xi1> + // CHECK: return [[expanded_1xi1]] : tensor<1xi1> + } + """ + + pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=QecCode.get("Shor913")),) + run_filecheck(program, pipeline) From a490bbf91c9ca529697269bdcc6ce1d9ea93164b Mon Sep 17 00:00:00 2001 From: Joey Carter Date: Thu, 25 Jun 2026 13:26:35 -0400 Subject: [PATCH 120/120] Fix incorrect indexing in diagonalizing gate ops --- .../transforms/qecp/convert_qecl_to_qecp.py | 5 +-- .../qecp/test_xdsl_convert_qecl_to_qecp.py | 41 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py index c174d87d0f..63cf1b5560 100644 --- a/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py +++ b/frontend/catalyst/python_interface/transforms/qecp/convert_qecl_to_qecp.py @@ -688,15 +688,14 @@ def create_measure_subroutine(self) -> func.FuncOp: with ImplicitBuilder(block): in_codeblock = cast(BlockArgument[qecp.PhysicalCodeblockType], block.args[0]) - # n = in_codeblock.type.n.value.data # Extract qubits at indices in codeblock where the op is not Identity extract_ops = [qecp.ExtractQubitOp(in_codeblock, i) for i in non_identity_idx] qubits = [ext_op.qubit for ext_op in extract_ops] # Insert diagonalizing gates - for i in non_identity_idx: - gate_op = pauli_z_gate_ops[i] + for i, cb_idx in enumerate(non_identity_idx): + gate_op = pauli_z_gate_ops[cb_idx] match gate_op: case "Z": pass diff --git a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py index 3af7fad0c7..7714fa7ac1 100644 --- a/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py +++ b/frontend/test/pytest/python_interface/transforms/qecp/test_xdsl_convert_qecl_to_qecp.py @@ -675,13 +675,13 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): builtin.module { // CHECK-LABEL: test_program func.func @test_program() { - // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 5> + // CHECK: [[cb0:%.+]] = "test.op"() : () -> !qecp.codeblock<1 x 6> %0 = "test.op"() : () -> !qecl.codeblock<1> // CHECK: [[mresp:%.+]], [[cb1:%.+]] = func.call @measure_transversal_TestCode([[cb0]]) : - // CHECK-SAME: (!qecp.codeblock<1 x 5>) -> (tensor<2xi1>, !qecp.codeblock<1 x 5>) + // CHECK-SAME: (!qecp.codeblock<1 x 6>) -> (tensor<4xi1>, !qecp.codeblock<1 x 6>) // CHECK: [[mresl:%.+]] = func.call @decode_physical_measurements_TestCode([[mresp]]) : - // CHECK-SAME: (tensor<2xi1>) -> tensor<1xi1> + // CHECK-SAME: (tensor<4xi1>) -> tensor<1xi1> // CHECK: [[zero:%.+]] = arith.constant 0 : index // CHECK: [[mres0:%.+]] = tensor.extract [[mresl]][[[zero]]] : tensor<1xi1> %mres0, %1 = qecl.measure %0[0] : i1, !qecl.codeblock<1> @@ -692,25 +692,34 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): return } // CHECK-LABEL: func.func private @measure_transversal_TestCode - // CHECK-SAME: ([[cb_in:%.+]]: !qecp.codeblock<1 x 5>) -> (tensor<2xi1>, !qecp.codeblock<1 x 5>) - // CHECK: [[q00:%.+]] = qecp.extract [[cb_in]][0] - // CHECK: [[q20:%.+]] = qecp.extract [[cb_in]][2] - // CHECK: [[m0:%.+]], [[q01:%.+]] = qecp.measure [[q00]] - // CHECK: [[m2:%.+]], [[q21:%.+]] = qecp.measure [[q20]] + // CHECK-SAME: ([[cb_0:%.+]]: !qecp.codeblock<1 x 6>) -> (tensor<4xi1>, !qecp.codeblock<1 x 6>) + // CHECK: [[q0_0:%.+]] = qecp.extract [[cb_0]][0] + // CHECK: [[q2_0:%.+]] = qecp.extract [[cb_0]][2] + // CHECK: [[q3_0:%.+]] = qecp.extract [[cb_0]][3] + // CHECK: [[q4_0:%.+]] = qecp.extract [[cb_0]][4] + // CHECK-DAG: [[m0:%.+]], [[q0_1:%.+]] = qecp.measure [[q0_0]] + // CHECK-DAG: [[m2:%.+]], [[q2_1:%.+]] = qecp.measure [[q2_0]] + // CHECK-DAG: [[q3_1:%.+]] = qecp.hadamard [[q3_0]] + // CHECK-DAG: [[m3:%.+]], [[q3_2:%.+]] = qecp.measure [[q3_1]] + // CHECK-DAG: [[q4_1:%.+]] = qecp.hadamard [[q4_0]] + // CHECK-DAG: [[q4_2:%.+]] = qecp.s [[q4_1]] adj + // CHECK-DAG: [[m4:%.+]], [[q4_3:%.+]] = qecp.measure [[q4_2]] // CHECK-NOT: qecp.measure - // CHECK: [[cb1:%.+]] = qecp.insert [[cb_in]][0], [[q01]] - // CHECK: [[cb2:%.+]] = qecp.insert [[cb1]][2], [[q21]] - // CHECK: [[m_2xi1:%.+]] = tensor.from_elements [[m0]], [[m2]] : tensor<2xi1> - // CHECK: func.return [[m_2xi1]], [[cb2]] : tensor<2xi1>, !qecp.codeblock<1 x 5> + // CHECK: [[cb_1:%.+]] = qecp.insert [[cb_0]][0], [[q0_1]] + // CHECK: [[cb_2:%.+]] = qecp.insert [[cb_1]][2], [[q2_1]] + // CHECK: [[cb_3:%.+]] = qecp.insert [[cb_2]][3], [[q3_2]] + // CHECK: [[cb_4:%.+]] = qecp.insert [[cb_3]][4], [[q4_3]] + // CHECK: [[m_4xi1:%.+]] = tensor.from_elements [[m0]], [[m2]], [[m3]], [[m4]] : tensor<4xi1> + // CHECK: func.return [[m_4xi1]], [[cb_4]] : tensor<4xi1>, !qecp.codeblock<1 x 6> // CHECK-LABEL: func.func private @decode_physical_measurements_TestCode - // CHECK-SAME: ([[in_mres_2xi1:%.+]]: tensor<2xi1>) -> tensor<1xi1> + // CHECK-SAME: ([[in_mres_4xi1:%.+]]: tensor<4xi1>) -> tensor<1xi1> // CHECK: [[c0:%.+]] = arith.constant false // CHECK: [[empty_i1:%.+]] = tensor.empty() : tensor // CHECK: [[init_i1:%.+]] = linalg.fill // CHECK-SAME: ins([[c0]] : i1) outs([[empty_i1]] : tensor) -> tensor // CHECK: [[reduced_i1:%.+]] = linalg.reduce - // CHECK-SAME: ins([[in_mres_2xi1]]:tensor<2xi1>) outs([[init_i1]]:tensor) + // CHECK-SAME: ins([[in_mres_4xi1]]:tensor<4xi1>) outs([[init_i1]]:tensor) // CHECK-SAME: dimensions = [0] // CHECK: ([[in:%.+]]: i1, [[out:%.+]]: i1) { // CHECK: [[xor:%.+]] = arith.xori [[in]], [[out]] : i1 @@ -721,10 +730,10 @@ def test_measure_toy_code(self, run_filecheck, get_generic_qec_code): } """ qec_code = get_generic_qec_code( - n=5, + n=6, k=1, d=3, - transversal_1q_gates={"z": ("Z", "I", "Z", "I", "I")}, + transversal_1q_gates={"z": ("Z", "I", "Z", "X", "Y", "I")}, ) pipeline = (ConvertQecLogicalToQecPhysicalPass(qec_code=qec_code),)