Skip to content

Add conversion pattern for qecl.qec ops to convert-qecl-to-qecp pass#2754

Draft
joeycarter wants to merge 14 commits intomainfrom
joeycarter/qecl-to-qecp-qec-cycle
Draft

Add conversion pattern for qecl.qec ops to convert-qecl-to-qecp pass#2754
joeycarter wants to merge 14 commits intomainfrom
joeycarter/qecl-to-qecp-qec-cycle

Conversation

@joeycarter
Copy link
Copy Markdown
Contributor

@joeycarter joeycarter commented Apr 24, 2026

Context: The convert-qecl-to-qecp dialect-conversion pass requires a rewrite pattern for qecl.qec operations.

Description of the Change: This pass adds the QecCycleOpConversion rewrite pattern to the convert-qecl-to-qecp dialect-conversion pass. This pattern replaces qecl.qec ops with a call to a subroutine that performs a single cycle of quantum error correction. The QEC protocol used here is the "textbook" protocol for a CSS code, which applies separate X and Z stabilizers to obtain the respective error-syndrome measurement (ESM) results in order to apply Z and X corrections, respectively. The decoding of the ESM is abstracted by the qecp.decode_esm_css op, which will eventually be lowered to a runtime library call to perform the actually decoding (not yet implemented).

This PR also adds a handful of utility functions to help with the conversion pattern, such as the function qecp.tanner_graph_lib.dense_tanner_graph_to_csc(), which converts a dense parity-check matrix to the equivalent matrix in compressed sparse column (CSC) form.

Example:

Before:

builtin.module @test_module {
  func.func @test_program() {
    %0 = "test.op"() : () -> !qecl.codeblock<1>
    %1 = qecl.qec %0 : !qecl.codeblock<1>
    func.return
  }
}

After (using Steane code):

builtin.module @test_module {
  %0 = arith.constant dense<[0, 0, 1, 0, 1, 2, 0, 2, 1, 1, 2, 2]> : tensor<12xi32>
  %1 = arith.constant dense<[0, 1, 3, 6, 8, 9, 11, 12]> : tensor<8xi32>
  %2 = qecp.assemble_tanner %0, %1 : tensor<12xi32>, tensor<8xi32> -> !qecp.tanner_graph<12, 8, i32>
  %3 = arith.constant dense<[0, 0, 1, 0, 1, 2, 0, 2, 1, 1, 2, 2]> : tensor<12xi32>
  %4 = arith.constant dense<[0, 1, 3, 6, 8, 9, 11, 12]> : tensor<8xi32>
  %5 = qecp.assemble_tanner %3, %4 : tensor<12xi32>, tensor<8xi32> -> !qecp.tanner_graph<12, 8, i32>
  func.func @test_program() {
    %6 = "test.op"() : () -> !qecp.codeblock<1 x 7>
    %7 = func.call @qec_cycle_Steane(%6) : (!qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7>
    func.return
  }
  func.func private @qec_cycle_Steane(%6: !qecp.codeblock<1 x 7>) -> !qecp.codeblock<1 x 7> {
    %7 = qecp.alloc_aux : !qecp.qubit<aux>
    %8 = qecp.alloc_aux : !qecp.qubit<aux>
    %9 = qecp.alloc_aux : !qecp.qubit<aux>
    %10 = qecp.hadamard %7 : !qecp.qubit<aux>
    %11 = qecp.hadamard %8 : !qecp.qubit<aux>
    %12 = qecp.hadamard %9 : !qecp.qubit<aux>
    %13 = qecp.extract %6[0] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %14 = qecp.extract %6[1] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %15 = qecp.extract %6[2] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %16 = qecp.extract %6[3] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %17 = qecp.extract %6[4] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %18 = qecp.extract %6[5] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %19 = qecp.extract %6[6] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %20, %21 = qecp.cnot %10, %13 : !qecp.qubit<aux>, !qecp.qubit<data>
    %22, %23 = qecp.cnot %20, %14 : !qecp.qubit<aux>, !qecp.qubit<data>
    %24, %25 = qecp.cnot %22, %15 : !qecp.qubit<aux>, !qecp.qubit<data>
    %26, %27 = qecp.cnot %24, %16 : !qecp.qubit<aux>, !qecp.qubit<data>
    %28, %29 = qecp.cnot %11, %23 : !qecp.qubit<aux>, !qecp.qubit<data>
    %30, %31 = qecp.cnot %28, %25 : !qecp.qubit<aux>, !qecp.qubit<data>
    %32, %33 = qecp.cnot %30, %17 : !qecp.qubit<aux>, !qecp.qubit<data>
    %34, %35 = qecp.cnot %32, %18 : !qecp.qubit<aux>, !qecp.qubit<data>
    %36, %37 = qecp.cnot %12, %31 : !qecp.qubit<aux>, !qecp.qubit<data>
    %38, %39 = qecp.cnot %36, %27 : !qecp.qubit<aux>, !qecp.qubit<data>
    %40, %41 = qecp.cnot %38, %35 : !qecp.qubit<aux>, !qecp.qubit<data>
    %42, %43 = qecp.cnot %40, %19 : !qecp.qubit<aux>, !qecp.qubit<data>
    %44 = qecp.insert %6[0], %21 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %45 = qecp.insert %44[1], %29 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %46 = qecp.insert %45[2], %37 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %47 = qecp.insert %46[3], %39 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %48 = qecp.insert %47[4], %33 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %49 = qecp.insert %48[5], %41 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %50 = qecp.insert %49[6], %43 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %51 = qecp.hadamard %26 : !qecp.qubit<aux>
    %52 = qecp.hadamard %34 : !qecp.qubit<aux>
    %53 = qecp.hadamard %42 : !qecp.qubit<aux>
    %54, %55 = qecp.measure %51 : i1, !qecp.qubit<aux>
    %56, %57 = qecp.measure %52 : i1, !qecp.qubit<aux>
    %58, %59 = qecp.measure %53 : i1, !qecp.qubit<aux>
    qecp.dealloc_aux %55 : !qecp.qubit<aux>
    qecp.dealloc_aux %57 : !qecp.qubit<aux>
    qecp.dealloc_aux %59 : !qecp.qubit<aux>
    %60 = tensor.from_elements %54, %56, %58 : tensor<3xi1>
    %61 = qecp.decode_esm_css(%60 : tensor<3xi1>) %2 : !qecp.tanner_graph<12, 8, i32> -> tensor<1xindex>
    %62 = arith.constant 0 : index
    %63 = arith.constant 1 : index
    %64 = arith.constant 1 : index
    %65 = scf.for %66 = %62 to %63 step %64 iter_args(%67 = %50) -> (!qecp.codeblock<1 x 7>) {
      %68 = tensor.extract %61[%66] : tensor<1xindex>
      %69 = arith.index_cast %68 : index to i64
      %70 = arith.constant -1 : i64
      %71 = arith.cmpi ne, %69, %70 : i64
      %72 = scf.if %71 -> (!qecp.codeblock<1 x 7>) {
        %73 = qecp.extract %67[%68] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
        %74 = qecp.z %73 : !qecp.qubit<data>
        %75 = qecp.insert %50[%68], %74 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
        scf.yield %75 : !qecp.codeblock<1 x 7>
      } else {
        scf.yield %67 : !qecp.codeblock<1 x 7>
      }
      scf.yield %72 : !qecp.codeblock<1 x 7>
    }
    %76 = qecp.alloc_aux : !qecp.qubit<aux>
    %77 = qecp.alloc_aux : !qecp.qubit<aux>
    %78 = qecp.alloc_aux : !qecp.qubit<aux>
    %79 = qecp.extract %72[0] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %80 = qecp.extract %72[1] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %81 = qecp.extract %72[2] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %82 = qecp.extract %72[3] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %83 = qecp.extract %72[4] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %84 = qecp.extract %72[5] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %85 = qecp.extract %72[6] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
    %86, %87 = qecp.cnot %79, %76 : !qecp.qubit<data>, !qecp.qubit<aux>
    %88, %89 = qecp.cnot %80, %87 : !qecp.qubit<data>, !qecp.qubit<aux>
    %90, %91 = qecp.cnot %81, %89 : !qecp.qubit<data>, !qecp.qubit<aux>
    %92, %93 = qecp.cnot %82, %91 : !qecp.qubit<data>, !qecp.qubit<aux>
    %94, %95 = qecp.cnot %88, %77 : !qecp.qubit<data>, !qecp.qubit<aux>
    %96, %97 = qecp.cnot %90, %95 : !qecp.qubit<data>, !qecp.qubit<aux>
    %98, %99 = qecp.cnot %83, %97 : !qecp.qubit<data>, !qecp.qubit<aux>
    %100, %101 = qecp.cnot %84, %99 : !qecp.qubit<data>, !qecp.qubit<aux>
    %102, %103 = qecp.cnot %96, %78 : !qecp.qubit<data>, !qecp.qubit<aux>
    %104, %105 = qecp.cnot %92, %103 : !qecp.qubit<data>, !qecp.qubit<aux>
    %106, %107 = qecp.cnot %100, %105 : !qecp.qubit<data>, !qecp.qubit<aux>
    %108, %109 = qecp.cnot %85, %107 : !qecp.qubit<data>, !qecp.qubit<aux>
    %110 = qecp.insert %72[0], %86 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %111 = qecp.insert %110[1], %94 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %112 = qecp.insert %111[2], %102 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %113 = qecp.insert %112[3], %104 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %114 = qecp.insert %113[4], %98 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %115 = qecp.insert %114[5], %106 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %116 = qecp.insert %115[6], %108 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
    %117, %118 = qecp.measure %93 : i1, !qecp.qubit<aux>
    %119, %120 = qecp.measure %101 : i1, !qecp.qubit<aux>
    %121, %122 = qecp.measure %109 : i1, !qecp.qubit<aux>
    qecp.dealloc_aux %118 : !qecp.qubit<aux>
    qecp.dealloc_aux %120 : !qecp.qubit<aux>
    qecp.dealloc_aux %122 : !qecp.qubit<aux>
    %123 = tensor.from_elements %117, %119, %121 : tensor<3xi1>
    %124 = qecp.decode_esm_css(%123 : tensor<3xi1>) %5 : !qecp.tanner_graph<12, 8, i32> -> tensor<1xindex>
    %125 = arith.constant 0 : index
    %126 = arith.constant 1 : index
    %127 = arith.constant 1 : index
    %128 = scf.for %129 = %125 to %126 step %127 iter_args(%130 = %116) -> (!qecp.codeblock<1 x 7>) {
      %131 = tensor.extract %124[%129] : tensor<1xindex>
      %132 = arith.index_cast %131 : index to i64
      %133 = arith.constant -1 : i64
      %134 = arith.cmpi ne, %132, %133 : i64
      %135 = scf.if %134 -> (!qecp.codeblock<1 x 7>) {
        %136 = qecp.extract %130[%131] : !qecp.codeblock<1 x 7> -> !qecp.qubit<data>
        %137 = qecp.x %136 : !qecp.qubit<data>
        %138 = qecp.insert %116[%131], %137 : !qecp.codeblock<1 x 7>, !qecp.qubit<data>
        scf.yield %138 : !qecp.codeblock<1 x 7>
      } else {
        scf.yield %130 : !qecp.codeblock<1 x 7>
      }
      scf.yield %135 : !qecp.codeblock<1 x 7>
    }
    func.return %135 : !qecp.codeblock<1 x 7>
  }
}

[sc-115303]

@joeycarter joeycarter changed the title [WIP] Add conversion pattern for qecl.qec ops Add conversion pattern for qecl.qec ops Apr 27, 2026
@joeycarter joeycarter marked this pull request as ready for review April 28, 2026 14:27
@joeycarter joeycarter changed the title Add conversion pattern for qecl.qec ops Add conversion pattern for qecl.qec ops to convert-qecl-to-qecp pass Apr 28, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 92.07921% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.01%. Comparing base (d21ea60) to head (3f6755d).

Files with missing lines Patch % Lines
..._interface/transforms/qecp/convert_qecl_to_qecp.py 91.30% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2754      +/-   ##
==========================================
+ Coverage   96.99%   97.01%   +0.02%     
==========================================
  Files         165      166       +1     
  Lines       18503    18599      +96     
  Branches     1783     1788       +5     
==========================================
+ Hits        17947    18044      +97     
+ Misses        399      397       -2     
- Partials      157      158       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +372 to +374
x_tanner_row_idx_array, x_tanner_col_ptr_array = dense_tanner_graph_to_csc(
self.qec_code.x_tanner
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong, self.qec_code.x_tanner is the parity check matrix, and dense_tanner_graph_to_csc just converts the dense matrix to a CSC matrix. But in fact what we want is the adjacency matrix of the Tanner graph in CSC format. Let me fix this...

@joeycarter joeycarter marked this pull request as draft April 29, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant