diff --git a/.agent/audits/controlled-rotation-quality.csv b/.agent/audits/controlled-rotation-quality.csv new file mode 100644 index 0000000000..a18a4ee8b3 --- /dev/null +++ b/.agent/audits/controlled-rotation-quality.csv @@ -0,0 +1,145 @@ +axis,controls,angle,backend,synthesis_ms,raw_cx,raw_one_qubit,raw_depth,raw_cx_depth,optimized_cx,optimized_one_qubit,optimized_depth,optimized_cx_depth,max_operator_error +rx,2,numeric,core,0.272258,4,6,10,4,4,5,9,4,2.1153413308660057e-16 +rx,2,numeric,qiskit,0.033504,8,15,18,8,8,7,13,8,3.298440587052649e-16 +rx,2,symbolic,core,0.365394,4,6,10,4,4,6,10,4,3.1401849173675503e-16 +rx,2,symbolic,qiskit,0.043104,8,15,18,8,8,9,15,8,3.3306690738754696e-16 +rx,3,numeric,core,0.382035,14,24,28,12,14,16,20,12,1.5331320705493272e-15 +rx,3,numeric,qiskit,0.069664,20,35,42,20,20,15,29,20,7.610516769633753e-16 +rx,3,symbolic,core,0.384211,14,24,28,12,14,20,24,12,1.2780539954372932e-15 +rx,3,symbolic,qiskit,0.086401,20,35,42,20,20,21,35,20,4.440892098500626e-16 +rx,4,numeric,core,0.433683,24,42,46,18,24,28,34,18,2.3361959382837703e-15 +rx,4,numeric,qiskit,0.189521,24,42,46,18,24,28,34,18,2.3361959382837703e-15 +rx,4,symbolic,core,0.448979,24,42,46,18,24,36,40,18,2.558937633260452e-15 +rx,4,symbolic,qiskit,0.087009,24,42,46,18,24,36,40,18,2.558937633260452e-15 +rx,5,numeric,core,0.26325,40,58,70,34,40,48,64,34,2.6801557172297703e-15 +rx,5,numeric,qiskit,0.074576,40,58,70,34,40,48,64,34,2.6801557172297703e-15 +rx,5,symbolic,core,0.265666,40,58,70,34,40,56,68,34,3.9563492527737815e-15 +rx,5,symbolic,qiskit,0.095857,40,58,70,34,40,56,68,34,3.9563492527737815e-15 +rx,6,numeric,core,0.350434,56,74,86,38,56,69,76,38, +rx,6,numeric,qiskit,0.089969,56,74,86,38,56,69,76,38, +rx,6,symbolic,core,0.342834,56,74,86,38,56,75,84,38, +rx,6,symbolic,qiskit,0.101105,56,74,86,38,56,75,84,38, +rx,7,numeric,core,0.426562,80,124,176,80,80,88,152,80, +rx,7,numeric,qiskit,0.250914,80,124,176,80,80,88,152,80, +rx,7,symbolic,core,0.934085,80,124,176,80,80,96,156,80, +rx,7,symbolic,qiskit,0.306498,80,124,176,80,80,96,156,80, +rx,8,numeric,core,1.081863,104,174,213,98,104,120,185,98, +rx,8,numeric,qiskit,0.24325,104,174,200,89,104,120,170,89, +rx,8,symbolic,core,1.085719,104,174,213,98,104,128,185,98, +rx,8,symbolic,qiskit,0.315538,104,174,200,89,104,128,170,89, +rx,9,numeric,core,1.210728,120,198,248,112,120,136,213,112, +rx,9,numeric,qiskit,0.253954,120,198,248,108,120,136,209,108, +rx,9,symbolic,core,1.215672,120,198,248,112,120,144,213,112, +rx,9,symbolic,qiskit,0.306066,120,198,248,108,120,144,209,108, +rx,10,numeric,core,1.361704,136,222,293,130,136,152,249,130, +rx,10,numeric,qiskit,0.277426,136,222,280,121,136,152,234,121, +rx,10,symbolic,core,1.386809,136,222,293,130,136,160,249,130, +rx,10,symbolic,qiskit,0.329666,136,222,280,121,136,160,234,121, +rx,16,numeric,core,2.290686,232,366,533,226,232,248,441,226, +rx,16,numeric,qiskit,0.335074,232,366,520,217,232,248,426,217, +rx,16,symbolic,core,2.301854,232,366,533,226,232,256,441,226, +rx,16,symbolic,qiskit,0.378083,232,366,520,217,232,256,426,217, +rx,32,numeric,core,4.60406,488,750,1173,482,488,504,953,482, +rx,32,numeric,qiskit,0.472531,488,750,1160,473,488,504,938,473, +rx,32,symbolic,core,4.589452,488,750,1173,482,488,512,953,482, +rx,32,symbolic,qiskit,0.519923,488,750,1160,473,488,512,938,473, +rx,64,numeric,core,9.109,1000,1518,2453,994,1000,1016,1977,994, +rx,64,numeric,qiskit,0.707028,1000,1518,2440,985,1000,1016,1962,985, +rx,64,symbolic,core,9.080984,1000,1518,2453,994,1000,1024,1977,994, +rx,64,symbolic,qiskit,0.842309,1000,1518,2440,985,1000,1024,1962,985, +ry,2,numeric,core,0.258562,4,4,8,4,4,4,8,4,2.220446049250313e-16 +ry,2,numeric,qiskit,0.036944,8,15,18,8,8,6,12,8,2.220446049250313e-16 +ry,2,symbolic,core,0.270545,4,4,8,4,4,4,8,4,2.220446049250313e-16 +ry,2,symbolic,qiskit,0.042288,8,15,18,8,8,6,12,8,3.3306690738754696e-16 +ry,3,numeric,core,0.350882,14,22,26,12,14,17,21,12,1.024360521199179e-15 +ry,3,numeric,qiskit,0.075584,20,35,42,20,20,14,28,20,2.220446049250313e-16 +ry,3,symbolic,core,0.355074,14,22,26,12,14,20,24,12,8.892273665708786e-16 +ry,3,symbolic,qiskit,0.095025,20,35,42,20,20,14,28,20,4.440892098500626e-16 +ry,4,numeric,core,0.406435,24,40,44,18,24,29,35,18,1.520397016336855e-15 +ry,4,numeric,qiskit,0.177777,24,40,44,18,24,29,35,18,1.520397016336855e-15 +ry,4,symbolic,core,0.408035,24,40,44,18,24,36,41,18,1.686470170725321e-15 +ry,4,symbolic,qiskit,0.207521,24,40,44,18,24,36,41,18,1.686470170725321e-15 +ry,5,numeric,core,0.562084,40,56,69,34,40,50,64,34,5.0035533349649525e-15 +ry,5,numeric,qiskit,0.197794,40,56,69,34,40,50,64,34,5.0035533349649525e-15 +ry,5,symbolic,core,0.558788,40,56,69,34,40,55,68,34,4.0974976367522364e-15 +ry,5,symbolic,qiskit,0.25357,40,56,69,34,40,55,68,34,4.0974976367522364e-15 +ry,6,numeric,core,0.719669,56,72,85,38,56,67,76,38, +ry,6,numeric,qiskit,0.200482,56,72,85,38,56,67,76,38, +ry,6,symbolic,core,0.345506,56,72,85,38,56,74,83,38, +ry,6,symbolic,qiskit,0.095505,56,72,85,38,56,74,83,38, +ry,7,numeric,core,0.440963,80,122,174,80,80,89,153,80, +ry,7,numeric,qiskit,0.095392,80,122,174,80,80,89,153,80, +ry,7,symbolic,core,0.42237,80,122,174,80,80,96,156,80, +ry,7,symbolic,qiskit,0.108529,80,122,174,80,80,96,156,80, +ry,8,numeric,core,0.506563,104,172,212,98,104,121,186,98, +ry,8,numeric,qiskit,0.237873,104,172,199,89,104,121,171,89, +ry,8,symbolic,core,1.086358,104,172,212,98,104,128,186,98, +ry,8,symbolic,qiskit,0.274962,104,172,199,89,104,128,171,89, +ry,9,numeric,core,1.203495,120,196,247,112,120,137,214,112, +ry,9,numeric,qiskit,0.243874,120,196,247,108,120,137,210,108, +ry,9,symbolic,core,1.210119,120,196,247,112,120,144,214,112, +ry,9,symbolic,qiskit,0.301426,120,196,247,108,120,144,210,108, +ry,10,numeric,core,1.335944,136,220,292,130,136,153,250,130, +ry,10,numeric,qiskit,0.25957,136,220,279,121,136,153,235,121, +ry,10,symbolic,core,1.356264,136,220,292,130,136,160,250,130, +ry,10,symbolic,qiskit,0.319554,136,220,279,121,136,160,235,121, +ry,16,numeric,core,2.213934,232,364,532,226,232,249,442,226, +ry,16,numeric,qiskit,0.346178,232,364,519,217,232,249,427,217, +ry,16,symbolic,core,2.235966,232,364,532,226,232,256,442,226, +ry,16,symbolic,qiskit,0.369299,232,364,519,217,232,256,427,217, +ry,32,numeric,core,2.109981,488,748,1172,482,488,505,954,482, +ry,32,numeric,qiskit,0.205905,488,748,1159,473,488,505,939,473, +ry,32,symbolic,core,2.101613,488,748,1172,482,488,512,954,482, +ry,32,symbolic,qiskit,0.233938,488,748,1159,473,488,512,939,473, +ry,64,numeric,core,4.250746,1000,1516,2452,994,1000,1017,1978,994, +ry,64,numeric,qiskit,0.349938,1000,1516,2439,985,1000,1017,1963,985, +ry,64,symbolic,core,4.418747,1000,1516,2452,994,1000,1024,1978,994, +ry,64,symbolic,qiskit,0.359011,1000,1516,2439,985,1000,1024,1963,985, +rz,2,numeric,core,0.094,4,4,8,4,4,4,8,4,0.0 +rz,2,numeric,qiskit,0.065872,4,4,8,4,4,4,8,4,0.0 +rz,2,symbolic,core,0.107504,4,4,8,4,4,4,8,4,0.0 +rz,2,symbolic,qiskit,0.074577,4,4,8,4,4,4,8,4,0.0 +rz,3,numeric,core,0.146417,14,22,26,12,14,17,21,12,9.71445146547012e-16 +rz,3,numeric,qiskit,0.070896,14,22,26,12,14,17,21,12,9.71445146547012e-16 +rz,3,symbolic,core,0.146593,14,22,26,12,14,20,24,12,8.473409486550037e-16 +rz,3,symbolic,qiskit,0.081153,14,22,26,12,14,20,24,12,8.473409486550037e-16 +rz,4,numeric,core,0.183121,24,40,44,18,24,29,35,18,1.3092278833360677e-15 +rz,4,numeric,qiskit,0.075681,24,40,44,18,24,29,35,18,1.3092278833360677e-15 +rz,4,symbolic,core,0.438195,24,40,44,18,24,36,41,18,1.6136471996107273e-15 +rz,4,symbolic,qiskit,0.213825,24,40,44,18,24,36,41,18,1.6136471996107273e-15 +rz,5,numeric,core,0.553923,40,56,69,34,40,48,64,34,2.538614803067886e-15 +rz,5,numeric,qiskit,0.199377,40,56,69,34,40,48,64,34,2.538614803067886e-15 +rz,5,symbolic,core,0.270114,40,56,69,34,40,55,68,34,2.871672278914067e-15 +rz,5,symbolic,qiskit,0.099201,40,56,69,34,40,55,68,34,2.871672278914067e-15 +rz,6,numeric,core,0.355427,56,72,85,38,56,69,76,38, +rz,6,numeric,qiskit,0.095633,56,72,85,38,56,69,76,38, +rz,6,symbolic,core,0.36133,56,72,85,38,56,74,83,38, +rz,6,symbolic,qiskit,0.102721,56,72,85,38,56,74,83,38, +rz,7,numeric,core,0.447603,80,122,174,80,80,89,153,80, +rz,7,numeric,qiskit,0.101792,80,122,174,80,80,89,153,80, +rz,7,symbolic,core,0.453491,80,122,174,80,80,96,156,80, +rz,7,symbolic,qiskit,0.112609,80,122,174,80,80,96,156,80, +rz,8,numeric,core,0.528067,104,172,212,98,104,121,186,98, +rz,8,numeric,qiskit,0.108993,104,172,199,89,104,121,171,89, +rz,8,symbolic,core,0.53146,104,172,212,98,104,128,186,98, +rz,8,symbolic,qiskit,0.119873,104,172,199,89,104,128,171,89, +rz,9,numeric,core,0.60346,120,196,247,112,120,137,214,112, +rz,9,numeric,qiskit,0.275169,120,196,247,108,120,137,210,108, +rz,9,symbolic,core,1.363753,120,196,247,112,120,144,214,112, +rz,9,symbolic,qiskit,0.341538,120,196,247,108,120,144,210,108, +rz,10,numeric,core,1.525946,136,220,292,130,136,153,250,130, +rz,10,numeric,qiskit,0.288994,136,220,279,121,136,153,235,121, +rz,10,symbolic,core,1.479721,136,220,292,130,136,160,250,130, +rz,10,symbolic,qiskit,0.145633,136,220,279,121,136,160,235,121, +rz,16,numeric,core,1.075383,232,364,532,226,232,249,442,226, +rz,16,numeric,qiskit,0.145457,232,364,519,217,232,249,427,217, +rz,16,symbolic,core,1.07559,232,364,532,226,232,256,442,226, +rz,16,symbolic,qiskit,0.163921,232,364,519,217,232,256,427,217, +rz,32,numeric,core,2.147598,488,748,1172,482,488,505,954,482, +rz,32,numeric,qiskit,0.482483,488,748,1159,473,488,505,939,473, +rz,32,symbolic,core,4.725454,488,748,1172,482,488,512,954,482, +rz,32,symbolic,qiskit,0.505475,488,748,1159,473,488,512,939,473, +rz,64,numeric,core,9.260025,1000,1516,2452,994,1000,1017,1978,994, +rz,64,numeric,qiskit,0.746389,1000,1516,2439,985,1000,1017,1963,985, +rz,64,symbolic,core,9.402778,1000,1516,2452,994,1000,1024,1978,994, +rz,64,symbolic,qiskit,0.842181,1000,1516,2439,985,1000,1024,1963,985, diff --git a/.agent/audits/controlled-synthesis-test-runtime.md b/.agent/audits/controlled-synthesis-test-runtime.md new file mode 100644 index 0000000000..ff505b28e0 --- /dev/null +++ b/.agent/audits/controlled-synthesis-test-runtime.md @@ -0,0 +1,49 @@ +# Controlled synthesis test runtime + +Status: implemented and validated. Baseline: +`1f25f40667079f909685992653549b12ce0e0be6` with the pending MCY support, shared +Pauli tests, and rotation CX budgets. The open PR is +[#2467](https://github.com/munich-quantum-toolkit/core/pull/2467). + +## Scope and contract + +Speed up `test_multi_controlled_decomposition.cpp` without removing control +widths, numeric or runtime angles, gate-count bounds, exact-phase comparisons, +borrowed-control restoration, or input/output IR and linearity verification. +Production synthesis and the Python test matrix are unchanged. + +## Findings + +The single-pass helper verified its output twice: once in `PassManager::run` and +again in an explicit `verify` call. Sampling the rotation resource test +attributed 658 samples to the first check and 677 to the second, compared with +712 in greedy rewriting. The helper now explicitly enables the pass manager's +verifier and retains the separate output linearity check. Input verification is +unchanged. Both output checks covered the same module after the same pass. + +The large Pauli full-operator tests spend most of their time in DD matrix +multiplication. Reusing packages solely to avoid allocations is not justified by +this profile. Switching the three matrix-only helpers to the existing +unitary-simulation DD configuration showed no benefit: the three-run median for +the 25 Pauli-at-eight-controls and numeric/runtime rotation tests changed from +5.276 to 5.294 seconds. This experiment was reverted. + +## Validation + +The baseline Debug binary passed all 297 decomposition tests in 42.4 seconds on +macOS ARM64 with AppleClang 21 and LLVM/MLIR 23.1. The run included a short +sampling interval, so it is diagnostic rather than a controlled speed ratio. + +A subsequent unprofiled three-run comparison of +`MultiControlledDecompositionTest.RotationsUseLinearResourcesWithoutExtraQubits` +used the original binary and the rebuilt single-verifier binary serially, with +no concurrent build. The median fell from 2.035 to 1.477 seconds (27%). All six +runs passed with all 96 axis, width, and angle-kind combinations retained. The +34-test MCY and rotation-resource filter also passed three times per binary. Its +median fell from 10.528 to 8.910 seconds (15%). Compilation and profiling time +are excluded from test durations. + +The final rebuilt binary passes all 297 tests. The full changed-file +`uvx nox -s cpp-lint -- ec799daa09f855bd0edcbc5592a5fedd90836516` check reports +zero findings, and `uvx nox -s lint` passes. The final full-suite run overlapped +with C++ lint and is not used for a whole-suite speedup claim. diff --git a/.agent/audits/pr2467-controlled-synthesis-review.md b/.agent/audits/pr2467-controlled-synthesis-review.md new file mode 100644 index 0000000000..3f2e5b9113 --- /dev/null +++ b/.agent/audits/pr2467-controlled-synthesis-review.md @@ -0,0 +1,150 @@ +# Controlled-synthesis audit and quality comparison + +Status: applied. Date: 2026-09-08. Original baseline: +`00a214ec3f49aab0eeee0352a05ef817100e8c55`. Changes rebased onto main +`4c5e45855e5bb50c42b68ddbf4f9a4dababb8737`. + +## Findings and disposition + +- **Identity modifier evaluation:** wide X/Y/Z synthesis can leave empty + `qco.ctrl` bodies after angle folding. The original head's DD consumer + rejected these valid identity regions. Main's shared `composeBodyMatrix` fix + in #2464 resolves this on rebase. No duplicate or Y-specific workaround is + needed. The 63-control coherent-state regression now passes without + canonicalization. +- **Unreachable HP24 machinery:** `mczCoreForWidth` uses specialized synthesis + below five controls and SP22 through 32. HP24 now asserts its actual minimum + of 33 controls and directly selects one dirty helper for odd widths, two for + even widths. Removed the inactive small-width table, ripple incrementer, + recursive relative-phase planner, thread-local cache, and estimate branches. + Active half-MCX widths exceed 11 and incrementer widths exceed 10, so those + removed alternatives had no production caller. Changing the crossover must + revisit this limit. This is a maintenance improvement, not a speed claim. +- **Wide-state oracle and coverage:** retained existing SP22 samples and added + 32/33/34, 47/48, and 63/64 controls for X/Y/Z. Compare the phase-sensitive + norm of `actual - expected` at `1e-11`, rather than DD node identity. The + coherent helper scopes DD arithmetic tolerance to `1e-15` and restores it + afterward; the default merging tolerance accumulated about `1e-9` error at 32 + controls. These selected states are not a full-operator bound. Independent + numeric and runtime rotation matrices now cover 2 through 10 controls. +- **Duplicate remapping:** remap each generated rotation half-plan in place; + remove unused `GateEmitter` remapping. This removes a second plan allocation + and move loop. Plans remain the sole owner of wire remapping. +- **Stale CLI descriptions:** both compiler options now name Y and rotations; + the generated pass description names the active HP24 dirty-helper choice. + +Source: +`mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp`. +Regression tests: the corresponding +`mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp`. + +## Contracts retained + +The modifier verifier owns restrictions on classical support operations. No +second dependency walker is needed. Balanced halves borrow opposite controls and +restore them coherently. Arbitrary relative-phase MCX replacements have not been +proved safe in the four-MCX rotation shell. RX uses Hadamard conjugation of RZ; +Y uses target S conjugation of X. Controlled `2*pi` rotations retain their +conditional minus sign. Native-target and minimum-width policies still apply. No +new cache, public API, or dependency is justified. + +## Reproducible Qiskit comparison + +Run `uv run --no-sync python test/bench/compare_controlled_rotations.py` with +the locally built package and Qiskit 2.5.2. The script records numeric and +symbolic RX/RY/RZ at 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 32, and 64 controls. Raw +rows from this run are in +[controlled-rotation-quality.csv](controlled-rotation-quality.csv). Core was +built with Clang/LLVM 23.1, Release, ThinLTO, and mold on ARM64. + +Both methods use exactly the input qubits. Nine-sample median synthesis times +exclude one warmup, input creation, import/export, basis conversion, and +routing. Both circuits receive the same `u,cx` normalization, then level-3 +optimization with seed zero and `qubits_initially_zero=False`. The CSV includes +CX count, one-qubit count, total depth, CX depth, and synthesis time. Numeric +and symbolic operators through five controls pass phase-sensitive checks, +including symbolic binding at `2*pi`. Timings are local sequential measurements, +not an end-to-end compilation comparison or stable performance guarantee. + +Numeric post-optimization examples (Core / Qiskit): + +| Gate | Controls | CX | Depth | Synthesis ms | +| ---- | -------: | ----------: | ----------: | ------------: | +| RX | 2 | 4 / 8 | 9 / 13 | 0.272 / 0.034 | +| RY | 3 | 14 / 20 | 21 / 28 | 0.351 / 0.076 | +| RY | 8 | 104 / 104 | 186 / 171 | 0.507 / 0.238 | +| RY | 16 | 232 / 232 | 442 / 427 | 2.214 / 0.346 | +| RY | 64 | 1000 / 1000 | 1978 / 1963 | 4.251 / 0.350 | + +Core saves CX gates for RX/RY at two and three controls. Other sampled CX counts +match, including all RZ cases and symbolic angles. Core's larger circuits are up +to 15 layers deeper. Core synthesis is slower in this measurement. Earlier +claims of uniformly faster Core synthesis included basis lowering in Qiskit's +timed work and are superseded by this comparison. + +## Improvement beyond Qiskit: measured candidate, deferred implementation + +Helper order affects scheduling even when CX count stays fixed. In an isolated +prototype, reverse the selected dirty-helper wires in both balanced half-MCXs, +keeping controls in order. Use Qiskit's exact `synth_mcx_n_dirty_i15` to +construct those halves, and the same four quarter-angle rotations and +normalization as above. This produces these numeric RY depths: + +| Controls | Core | Qiskit | Reversed-helper prototype | CX (all three) | +| -------- | ---: | -----: | ------------------------: | -------------: | +| 9 | 214 | 210 | 204 | 120 | +| 10 | 250 | 235 | 232 | 136 | +| 16 | 442 | 427 | 388 | 232 | +| 32 | 954 | 939 | 804 | 488 | +| 64 | 1978 | 1963 | 1636 | 1000 | + +At eight controls, balanced reversed helpers give depth 180, worse than Qiskit's +171. A 5+3 split instead reduces CX from 104 to 96 but has depth 185. Neither +candidate dominates at every width. The reversed balanced prototype at nine +controls and both eight-control splits pass full phase-sensitive operator +comparisons with maximum element error below `1.2e-14`. + +Minimal reproduction, after importing `QuantumCircuit`, `transpile`, and +`qiskit.synthesis.synth_mcx_n_dirty_i15`: + +```python +k = 64 +first = (k + 1) // 2 +halves = [] +for start, count in ((0, first), (first, k - first)): + plan = synth_mcx_n_dirty_i15(count) + spare = [i for i in range(k) if i < start or i >= start + count] + spare = spare[: plan.num_qubits - count - 1] + halves.append((plan, list(range(start, start + count)) + [k] + spare[::-1])) +circuit = QuantumCircuit(k + 1) +for _ in range(2): + circuit.compose(*halves[0], inplace=True) + circuit.ry(-0.73 / 4, k) + circuit.compose(*halves[1], inplace=True) + circuit.ry(0.73 / 4, k) +output = transpile( + circuit, + basis_gates=["u", "cx"], + optimization_level=3, + seed_transpiler=0, + qubits_initially_zero=False, +) +assert output.count_ops()["cx"] == 1000 +assert output.depth() == 1636 +``` + +This establishes room beyond Qiskit's current public synthesis output, not +optimality or a ready Core patch. Before implementation, measure Core's own +helper ordering across all axes, symbolic expressions, and routing targets; +retain the exact-restoration tests. A width-specific split policy needs an +explicit objective because CX count and depth disagree at eight controls. The +existing construction has linear CX count; results requiring additional +clean/dirty qubits do not establish an improvement under this no-extra-qubit +contract. + +## Validation + +The native decomposition binary passes all 303 tests, including both active HP24 +dirty-helper modes and numeric/runtime full operators through ten controls. The +benchmark completes all 144 backend rows. Final lint and Python validation are +recorded in the implementation plan. diff --git a/.agent/plans/controlled-rotations.md b/.agent/plans/controlled-rotations.md new file mode 100644 index 0000000000..dc62386fa9 --- /dev/null +++ b/.agent/plans/controlled-rotations.md @@ -0,0 +1,69 @@ +# Multi-controlled Pauli rotations + +Status: implemented. Rebased onto main +`4c5e45855e5bb50c42b68ddbf4f9a4dababb8737`. + +## Goal and scope + +`decompose-multi-controlled` supports RX, RY, and RZ with numeric and symbolic +angles. It preserves phase, wire order, native target operations, and the +existing `min-qubits` policy. Synthesis uses no additional qubits and scales +linearly in entangling gates before routing. + +The owning implementation is +`mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp`. +Pass tests reside in the existing decomposition unit-test file. No new public +API or dependency was added. + +## Decisions + +- Lower controlled Y as `S†`, MCX, then `S` on the target, reusing the existing + MCX decomposition and its width and native-target policies. +- For RY and RZ, split controls into two balanced groups and alternate their MCX + operations with quarter-angle rotations. Borrow controls from the other group + through Core's exact dirty-helper MCX decomposition. Helpers must be restored + coherently, including relative phases. +- Obtain RX by Hadamard conjugation of RZ. Controlled rotations through `2*pi` + retain their conditional phase; the phase-gate normalization rules do not + apply. +- Reuse the existing fixed-angle MCX plans. Keep symbolic rotation angles as SSA + values, including computations defined within a control region. + +## Validation + +The complete `mqt-core-mlir-unittest-decomposition` binary passes 303 tests. +These check phase-exact operators, runtime and region-local angles, native +target and threshold policy, and numeric and symbolic rotation CX budgets +through 64 controls. Shared Pauli tests cover X, Y, and Z with the same CX +counts, including coherent states at synthesis boundaries. + +The Python `test_qco_program_decomposes_multi_controlled` API test covers X, Y, +RX, RY, and RZ, including the minimum-width argument and its error handling. One +symbolic RY round trip checks export and binding of generated angle expressions. +These six cases pass; synthesis matrices and resource bounds remain in the +native tests. + +The test pass manager verifies each output once; the helper retains separate +input/output linearity checks. The runtime comparison and rejected cache +experiment are recorded in +[`controlled-synthesis-test-runtime.md`](../audits/controlled-synthesis-test-runtime.md). + +MCY import, decomposition, and export preserve the exact operator at 2, 3, and 5 +controls. General lint, stub generation, and whole-changed-file C++ lint pass +with no remaining findings. Stub generation leaves no diff. The complete +`uvx nox --non-interactive -s docs` HTML build passes, as does the native +`mlir-doc` target. The compiler help lists the supported gate families. + +## Performance and limits + +The reproducible benchmark and applied audit findings are recorded in +[`pr2467-controlled-synthesis-review.md`](../audits/pr2467-controlled-synthesis-review.md). +Core uses fewer CX gates than Qiskit 2.5.2 for two- and three-control RX/RY and +matches other sampled counts through 64 controls. Larger outputs are up to 15 +layers deeper. Fair synthesis-only timings are slower for Core in this run; +previous timing claims are superseded. + +A reversed-helper prototype beats Qiskit's depth at sampled widths from nine +through 64 controls, with unchanged CX counts. An unbalanced eight-control split +saves eight CX gates but increases depth. These are measured follow-up +candidates, not production changes or an optimality claim. diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ff540113..b327b87917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,7 @@ releases may include breaking changes. [**@denialhaag**], [**@MatthiasReumann**], [**@simon1hofmann**]) - ✨ Add multi-qubit decomposition, fusion, and target-native synthesis passes ([#1774], [#1802], [#1803], [#1809], [#1810], [#1814], [#1832], [#1850], - [#1865], [#1961], [#1996], [#1998], [#2001]) ([**@simon1hofmann**], + [#1865], [#1961], [#1996], [#1998], [#2001], [#2467]) ([**@simon1hofmann**], [**@burgholzer**]) #### Other additions @@ -927,6 +927,7 @@ for previous changelogs._ +[#2467]: https://github.com/munich-quantum-toolkit/core/pull/2467 [#2457]: https://github.com/munich-quantum-toolkit/core/pull/2457 [#2436]: https://github.com/munich-quantum-toolkit/core/pull/2436 [#2421]: https://github.com/munich-quantum-toolkit/core/pull/2421 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 9be16696e2..c2940f405a 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -1232,9 +1232,10 @@ operations.)pb"); &BooleanMemberAdapter< &mlir::QCOProgram::decomposeMultiControlled>::call, nb::kw_only(), "min_qubits"_a = 3, - "Decompose controlled X/Z/SWAP gates, qco.rccx, and constant-angle " - "phase gates that act on at least min_qubits qubits (min_qubits " - "must be at least 3; default 3 means wider than two-qubit).") + "Decompose controlled X/Y/Z/SWAP and RX/RY/RZ gates, qco.rccx, and " + "constant-angle phase gates that act on at least min_qubits qubits " + "(min_qubits must be at least 3; default 3 means wider than " + "two-qubit).") .def("compile_for_target", &BooleanMemberAdapter<&mlir::QCOProgram::compileForTarget>::call, "target_environment"_a, nb::kw_only(), "enable_timing"_a = false, diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index d4c3bdd59b..9660279ace 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -277,9 +277,9 @@ class QCOProgram final : public Program { /// Prepare the program for qubit reuse and reuse eligible qubits. [[nodiscard]] bool runQubitReusePipeline(); - /// Decompose controlled X/Z/SWAP gates, `qco.rccx`, and constant-angle phase - /// gates that act on at least @p minQubits qubits (@p minQubits must be at - /// least 3; default 3 means wider than two-qubit). + /// Decompose controlled X/Y/Z/SWAP and RX/RY/RZ gates, `qco.rccx`, and + /// constant-angle phase gates that act on at least @p minQubits qubits + /// (@p minQubits must be at least 3; default 3 means wider than two-qubit). [[nodiscard]] bool decomposeMultiControlled(uint64_t minQubits = 3); /// Compile this program for a target in place. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 85e88d0cb6..10e9f66175 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -390,13 +390,15 @@ def DecomposeMultiControlled : Pass<"decompose-multi-controlled", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect", "::mlir::arith::ArithDialect"]; - let summary = "Decompose controlled X/Z/phase/SWAP gates and qco.rccx that " - "act on at least min-qubits qubits"; + let summary = "Decompose controlled X/Y/Z/rotation/phase/SWAP gates and " + "qco.rccx that act on at least min-qubits qubits"; let description = [{ Decomposes multi-qubit controlled operations that act on at least `min-qubits` qubits (default 3: everything wider than a two-qubit gate). - Supported shapes: `qco.ctrl` with a `qco.x`, `qco.z`, `qco.swap`, or - constant-angle `qco.p` body, and `qco.rccx`. + Supported shapes: `qco.ctrl` with a sole `qco.x`, `qco.y`, `qco.z`, + `qco.rx`, `qco.ry`, `qco.rz`, `qco.swap`, or constant-angle `qco.p` body, and + `qco.rccx`. Rotation angles may be constants or runtime SSA values, + including classical expressions inside the control region. | Family | Width (qubits) | Decomposition | | ------ | -------------- | ------------- | @@ -404,7 +406,9 @@ def DecomposeMultiControlled | X/Z | 4 | Elementary CCCX/CCCZ | | X/Z | 5 | Specialized ancilla-free relative-phase `C^4(Z)` | | X/Z | 6–33 | da Silva-Park SP22 MCP(π) core (`H · MCP(π) · H` for X) | - | X/Z | ≥34 | Huang-Palsberg (HP24) borrowed-helper synthesis with a compile-time CX policy table | + | X/Z | ≥34 | Huang-Palsberg (HP24) borrowed-helper synthesis with one dirty helper for odd control counts and two for even counts | + | Y | ≥3 | X decomposition with `S†` before and `S` after on the target | + | RX/RY/RZ | ≥3 | Balanced control halves with exact MCX and quarter-angle rotations; RX uses H-conjugated RZ | | Phase | 3 | Optimized `C^2(P)` | | Phase | 4–5 | Vale (Barenco-relative residual) | | Phase | ≥6 | da Silva-Park SP22 linear-depth | @@ -414,6 +418,11 @@ def DecomposeMultiControlled Width is the total number of qubits the gate acts on (controls plus targets). + Rotation synthesis uses a linear number of gates and no additional + qubits. Each half-MCX may borrow controls from the opposite half and + restores them coherently. The decomposition preserves phase, including + the conditional phase of a rotation by `2π`. + For controlled SWAP, `C ∪ {b}` is the original control set together with SWAP target `b`, and the MCX target is the other SWAP qubit `a`. The emitted MCX is then lowered by the X path under the same `min-qubits` @@ -425,9 +434,9 @@ def DecomposeMultiControlled }]; let options = [Option< "minQubits", "min-qubits", "uint64_t", "3", - "Decompose controlled X/Z/phase/SWAP gates and qco.rccx that act on at " - "least this many qubits (must be at least 3; default 3 means wider than " - "two-qubit).">]; + "Decompose controlled X/Y/Z/rotation/phase/SWAP gates and qco.rccx " + "that act on at least this many qubits (must be at least 3; default 3 " + "means wider than two-qubit).">]; } #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp index 4643f25613..9c71f2ec9f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -27,7 +27,6 @@ #include #include -#include #include #include #include @@ -48,16 +47,6 @@ namespace { // the publications cited at the respective algorithms. enum class Hp24DirtyMode : uint8_t { OneDirty, TwoDirty }; -enum class Hp24IncrementerKind : uint8_t { Ripple, Partitioned }; -enum class Hp24HalfMcxKind : uint8_t { RelativePhaseTernary, BorrowedHelper }; -struct Hp24Policy { - Hp24DirtyMode dirtyMode = Hp24DirtyMode::TwoDirty; - Hp24IncrementerKind incrementerKind = Hp24IncrementerKind::Ripple; - size_t incrementerRippleMaxWidth = 10; - Hp24HalfMcxKind halfMcxKind = Hp24HalfMcxKind::RelativePhaseTernary; - size_t halfMcxBorrowedHelperMinControls = 11; -}; - enum class ControlledTarget : uint8_t { X, Z, Phase }; constexpr double K_PI = std::numbers::pi; @@ -65,9 +54,8 @@ constexpr double K_PI8 = K_PI / 8.0; class GateEmitter { public: - GateEmitter(OpBuilder& builder, Location loc, SmallVector& wires, - ArrayRef remap = {}) - : builder_(&builder), loc_(loc), wires_(&wires), remap_(remap) {} + GateEmitter(OpBuilder& builder, Location loc, SmallVector& wires) + : builder_(&builder), loc_(loc), wires_(&wires) {} // Single- and two-qubit primitives void h(size_t q) { @@ -82,6 +70,14 @@ class GateEmitter { setWire(q, POp::create(*builder_, loc_, wire(q), theta).getOutputQubit(0)); } + void ry(size_t q, Value theta) { + setWire(q, RYOp::create(*builder_, loc_, wire(q), theta).getOutputQubit(0)); + } + + void rz(size_t q, Value theta) { + setWire(q, RZOp::create(*builder_, loc_, wire(q), theta).getOutputQubit(0)); + } + void t(size_t q) { setWire(q, TOp::create(*builder_, loc_, wire(q)).getOutputQubit(0)); } @@ -248,22 +244,13 @@ class GateEmitter { setWire(target, ctrlOp.getTargetsOut()[0]); } - [[nodiscard]] size_t wireIndex(size_t local) const { - return remap_.empty() ? local : remap_[local]; - } + [[nodiscard]] Value wire(size_t local) const { return (*wires_)[local]; } - [[nodiscard]] Value wire(size_t local) const { - return (*wires_)[wireIndex(local)]; - } - - void setWire(size_t local, Value value) { - (*wires_)[wireIndex(local)] = value; - } + void setWire(size_t local, Value value) { (*wires_)[local] = value; } OpBuilder* builder_; Location loc_; SmallVector* wires_; - ArrayRef remap_; }; //===----------------------------------------------------------------------===// @@ -323,53 +310,20 @@ struct BorrowedControlPartition { return 2 * (2 + (10 * (numControls - 3))); } -[[nodiscard]] static size_t estimateRelativePhaseMcxOps(size_t numControls) { - if (numControls <= 2) { - return 1; - } - const size_t num3 = numControls / 3; - const size_t num2 = (numControls - num3) / 2; - const size_t num1 = numControls - num3 - num2; - return 9 + (4 * estimateRelativePhaseMcxOps(num3)) + - (2 * estimateRelativePhaseMcxOps(num2)) + - (2 * estimateRelativePhaseMcxOps(num1)); -} - [[nodiscard]] static size_t estimateIncrementerPartitionedOps(size_t n) { return (16 * n) + 4; } -[[nodiscard]] static size_t estimateIncrementerRippleOps(size_t n) { - size_t total = 1; - for (size_t width = 1; width < n; ++width) { - total += estimateBorrowedHelperMcxOps(width); - } - return total; -} - -[[nodiscard]] static size_t estimateIncrementerOps(size_t n, - const Hp24Policy& policy) { - if (policy.incrementerKind == Hp24IncrementerKind::Ripple && - n <= policy.incrementerRippleMaxWidth) { - return estimateIncrementerRippleOps(n); - } - return estimateIncrementerPartitionedOps(n); -} - [[nodiscard]] static size_t -estimateBorrowedDirtyIncrementerOps(size_t n, const Hp24Policy& policy, +estimateBorrowedDirtyIncrementerOps(size_t n, Hp24DirtyMode dirtyMode, bool flagAdd) { - const bool oneDirty = policy.dirtyMode == Hp24DirtyMode::OneDirty; + const bool oneDirty = dirtyMode == Hp24DirtyMode::OneDirty; const size_t k = oneDirty ? (n + 1) / 2 : (n + 2) / 2; const size_t lowIncrementWidth = oneDirty ? k : (1 + n - k); const size_t incrementerOps = - estimateIncrementerOps(lowIncrementWidth, policy); - const size_t halfMcxOps = - policy.halfMcxKind == Hp24HalfMcxKind::RelativePhaseTernary && - k < policy.halfMcxBorrowedHelperMinControls - ? estimateRelativePhaseMcxOps(k) - : estimateBorrowedHelperMcxOps(k); - const size_t highIncrementOps = estimateIncrementerOps(k, policy); + estimateIncrementerPartitionedOps(lowIncrementWidth); + const size_t halfMcxOps = estimateBorrowedHelperMcxOps(k); + const size_t highIncrementOps = estimateIncrementerPartitionedOps(k); return (2 * incrementerOps) + (2 * halfMcxOps) + highIncrementOps + (2 * (n - k)) + 4 + (flagAdd ? 0 : (2 * n)); } @@ -452,39 +406,6 @@ static void appendRemapped(CircuitPlan& dest, CircuitPlan src, // Phase-π core on all-ones; no clean helpers (borrow target / a control as // dirty). Callers use `MCZ = core` and `MCX = H . core . H` on the target. -static constexpr size_t K_ONE_DIRTY_MIN_CONTROLS = 23; -static constexpr size_t K_HP24_POLICY_TABLE_MIN = 4; -static constexpr size_t K_HP24_POLICY_TABLE_MAX = 24; - -[[nodiscard]] static constexpr Hp24Policy -defaultHp24Policy(size_t numControls) { - Hp24Policy policy; - if (numControls >= K_ONE_DIRTY_MIN_CONTROLS && (numControls % 2 == 1)) { - policy.dirtyMode = Hp24DirtyMode::OneDirty; - } - return policy; -} - -// HP24 policies for k=4…24 (`selectHp24Policy`). -static constexpr auto K_HP24_POLICY_TABLE = [] { - std::array table{}; - for (size_t k = 0; k <= K_HP24_POLICY_TABLE_MAX; ++k) { - table[k] = defaultHp24Policy(k); - } - table[7].dirtyMode = Hp24DirtyMode::OneDirty; - table[21].halfMcxBorrowedHelperMinControls = 13; - table[22].halfMcxBorrowedHelperMinControls = 13; - return table; -}(); - -[[nodiscard]] static Hp24Policy selectHp24Policy(size_t numControls) { - if (numControls >= K_HP24_POLICY_TABLE_MIN && - numControls <= K_HP24_POLICY_TABLE_MAX) { - return K_HP24_POLICY_TABLE[numControls]; - } - return defaultHp24Policy(numControls); -} - // HP24 §4.3 relative-phase Toffoli gadget (and its reverse-order adjoint). static void appendGadget(CircuitPlan& plan, size_t q0, size_t q1, size_t q2, bool invert) { @@ -598,122 +519,19 @@ static CircuitPlan planIncrementerPartitioned(size_t n) { return plan; } -// HP24 Fig. 10 ripple incrementer `U^n_{+1}` (narrow registers). -static CircuitPlan planIncrementerRipple(size_t n) { - CircuitPlan plan; - plan.ops.reserve(estimateIncrementerRippleOps(n)); - SmallVector wires; - for (size_t width = n - 1; width >= 1; --width) { - wires.clear(); - for (size_t q = 0; q <= width; ++q) { - wires.push_back(q); - } - for (size_t q = n + 1; q < 2 * n; ++q) { - wires.push_back(q); - } - appendRemapped(plan, planBorrowedHelperMcx(width), wires); - } - plan.append({.kind = PlanOpKind::X, .wires = {0}}); - return plan; -} - -// Leaf `U^n_{+1}`: Fig. 10 ripple when narrow, partitioned carry ladder when -// wide (crossover via policy; Fig. 6 recursion is in -// `planBorrowedDirtyIncrementer`). -static CircuitPlan planIncrementer(size_t n, const Hp24Policy& policy) { - if (policy.incrementerKind == Hp24IncrementerKind::Ripple && - n <= policy.incrementerRippleMaxWidth) { - return planIncrementerRipple(n); - } - return planIncrementerPartitioned(n); -} - -// HP24 §4.3 relative-phase MCX (ternary ladder); phases cancel in pairs. -static CircuitPlan planRelativePhaseMcx(size_t numControls) { - // Memoize by width: the recursive ladder rebuilds the same sub-widths many - // times, and half-MCX widths stay well below this bound in practice. - constexpr size_t kCacheMax = 32; - thread_local std::array, kCacheMax + 1> cache{}; - if (numControls <= kCacheMax && cache[numControls].has_value()) { - return *cache[numControls]; - } - - CircuitPlan plan; - const size_t target = numControls; - if (numControls == 1) { - plan.append({.kind = PlanOpKind::CX, .wires = {0, 1}}); - } else if (numControls == 2) { - plan.append({.kind = PlanOpKind::RCCX, .wires = {0, 1, 2}}); - } else if (numControls >= 3) { - plan.ops.reserve(estimateRelativePhaseMcxOps(numControls)); - - // Balanced three-way split of the controls into blocks of sizes num1, - // num2, num3 (num3 = floor(k/3) is the largest split that keeps the ladder - // balanced across the recursion). - const size_t num3 = numControls / 3; - const size_t num2 = (numControls - num3) / 2; - const size_t num1 = numControls - num3 - num2; - const size_t block2Begin = num1; - const size_t block3Begin = num1 + num2; - const size_t controlsEnd = numControls; - - SmallVector wires; - const auto ladderStep = [&](size_t begin, size_t end, size_t width, - bool positive) { - plan.append({ - .kind = PlanOpKind::P, - .wires = {target}, - .angle = positive ? K_PI8 : -K_PI8, - }); - wires.clear(); - for (size_t q = begin; q < end; ++q) { - wires.push_back(q); - } - wires.push_back(target); - appendRemapped(plan, planRelativePhaseMcx(width), wires); - }; - - plan.append({.kind = PlanOpKind::H, .wires = {target}}); - ladderStep(block3Begin, controlsEnd, num3, true); - ladderStep(block2Begin, block3Begin, num2, false); - ladderStep(block3Begin, controlsEnd, num3, true); - ladderStep(0, block2Begin, num1, false); - ladderStep(block3Begin, controlsEnd, num3, true); - ladderStep(block2Begin, block3Begin, num2, false); - ladderStep(block3Begin, controlsEnd, num3, true); - ladderStep(0, block2Begin, num1, false); - plan.append({.kind = PlanOpKind::H, .wires = {target}}); - } - - if (numControls <= kCacheMax) { - cache[numControls] = plan; - } - return plan; -} - -// Ternary relative-phase MCX below helperMin; else borrowed-helper MCX. -static CircuitPlan planRelativePhaseMcxWide(size_t numControls, - const Hp24Policy& policy) { - if (policy.halfMcxKind == Hp24HalfMcxKind::RelativePhaseTernary && - numControls < policy.halfMcxBorrowedHelperMinControls) { - return planRelativePhaseMcx(numControls); - } - return planBorrowedHelperMcx(numControls); -} - // HP24 Fig. 6/8 partitioned incrementer. One-dirty borrows the target; // two-dirty also borrows the top control. `flagAdd == false` yields `U_{-1}` // (Eq. (7)). static CircuitPlan planBorrowedDirtyIncrementer(size_t n, bool flagAdd, - const Hp24Policy& policy) { + Hp24DirtyMode dirtyMode) { CircuitPlan plan; - const bool oneDirty = policy.dirtyMode == Hp24DirtyMode::OneDirty; + const bool oneDirty = dirtyMode == Hp24DirtyMode::OneDirty; const size_t numDirty = oneDirty ? 1 : 2; const size_t k = oneDirty ? (n + 1) / 2 : (n + 2) / 2; const size_t helper = n; const size_t helper2 = n + 1; const size_t lowIncrementWidth = oneDirty ? k : (1 + n - k); - plan.ops.reserve(estimateBorrowedDirtyIncrementerOps(n, policy, flagAdd)); + plan.ops.reserve(estimateBorrowedDirtyIncrementerOps(n, dirtyMode, flagAdd)); const auto flipRegister = [&] { for (size_t q = 0; q < n; ++q) { @@ -764,14 +582,13 @@ static CircuitPlan planBorrowedDirtyIncrementer(size_t n, bool flagAdd, } const auto incrementLow = [&] { - appendRemapped(plan, planIncrementer(lowIncrementWidth, policy), + appendRemapped(plan, planIncrementerPartitioned(lowIncrementWidth), lowIncrementWires); }; const auto halfMcx = [&] { - // Relative-phase / borrowed-helper MCX: the high half (and optional - // helper2) on `halfMcxWires` are dirty workspace and must stay in the - // remap map — a bare NestedMCX would drop them. - appendRemapped(plan, planRelativePhaseMcxWide(k, policy), halfMcxWires); + /// The high half (and optional helper2) supplies dirty workspace. + /// Keep those wires in the map; a bare NestedMCX would omit them. + appendRemapped(plan, planBorrowedHelperMcx(k), halfMcxWires); }; const auto fanOutHelper = [&] { for (size_t q = k; q < n; ++q) { @@ -791,7 +608,7 @@ static CircuitPlan planBorrowedDirtyIncrementer(size_t n, bool flagAdd, plan.append({.kind = PlanOpKind::X, .wires = {helper}}); halfMcx(); fanOutHelper(); - appendRemapped(plan, planIncrementer(k, policy), highIncrementWires); + appendRemapped(plan, planIncrementerPartitioned(k), highIncrementWires); if (!flagAdd) { flipRegister(); @@ -800,26 +617,29 @@ static CircuitPlan planBorrowedDirtyIncrementer(size_t n, bool flagAdd, } // HP24 Theorem 4.4: `C^{n-1}(p(π))` via dirty incrementer + phase ladder. -static CircuitPlan planHp24Core(size_t n, const Hp24Policy& policy) { +static CircuitPlan planHp24Core(size_t numControls) { + /// Narrower widths use the specialized or SP22 constructions. + assert(numControls >= 33 && "HP24 requires at least 33 controls"); + const size_t n = numControls + 1; + const auto dirtyMode = + numControls % 2 == 1 ? Hp24DirtyMode::OneDirty : Hp24DirtyMode::TwoDirty; CircuitPlan plan; - const size_t numControls = n - 1; const size_t target = n - 1; const size_t topControl = n - 2; - const size_t registerWidth = policy.dirtyMode == Hp24DirtyMode::OneDirty - ? numControls - : numControls - 1; + const size_t registerWidth = + dirtyMode == Hp24DirtyMode::OneDirty ? numControls : numControls - 1; plan.ops.reserve( - estimateBorrowedDirtyIncrementerOps(registerWidth, policy, true) + - estimateBorrowedDirtyIncrementerOps(registerWidth, policy, false) + + estimateBorrowedDirtyIncrementerOps(registerWidth, dirtyMode, true) + + estimateBorrowedDirtyIncrementerOps(registerWidth, dirtyMode, false) + (2 * (numControls - 1)) + 1); SmallVector registerWires(n); std::iota(registerWires.begin(), registerWires.end(), 0U); - if (policy.dirtyMode == Hp24DirtyMode::OneDirty) { + if (dirtyMode == Hp24DirtyMode::OneDirty) { const auto increment = [&](bool add) { appendRemapped(plan, - planBorrowedDirtyIncrementer(numControls, add, policy), + planBorrowedDirtyIncrementer(numControls, add, dirtyMode), registerWires); }; increment(true); @@ -839,9 +659,9 @@ static CircuitPlan planHp24Core(size_t n, const Hp24Policy& policy) { } const auto increment = [&](bool add) { - appendRemapped(plan, - planBorrowedDirtyIncrementer(numControls - 1, add, policy), - registerWires); + appendRemapped( + plan, planBorrowedDirtyIncrementer(numControls - 1, add, dirtyMode), + registerWires); }; increment(true); double phi = -K_PI; @@ -1033,7 +853,7 @@ static CircuitPlan planMczRelativePhaseK4() { return plan; } -static CircuitPlan mczCoreForWidth(size_t numControls, size_t numWires); +static CircuitPlan mczCoreForWidth(size_t numControls); static SmallVector synthesizeMultiControlled(OpBuilder& builder, Location loc, ValueRange controls, @@ -1043,7 +863,7 @@ synthesizeMultiControlled(OpBuilder& builder, Location loc, ValueRange controls, const size_t targetIdx = controls.size(); GateEmitter emitter(builder, loc, wires); - const CircuitPlan plan = mczCoreForWidth(controls.size(), wires.size()); + const CircuitPlan plan = mczCoreForWidth(controls.size()); if (gate == ControlledTarget::X) { emitter.h(targetIdx); lowerPlan(emitter, plan); @@ -1074,6 +894,71 @@ static BorrowedControlPartition partitionControls(size_t numControls) { return {.k1 = (numControls + 1) / 2, .k2 = numControls / 2}; } +/// Synthesize a controlled Pauli rotation using X R(a) X = R(-a) for Y/Z. +static SmallVector +synthesizeMultiControlledRotation(OpBuilder& builder, Location loc, + ValueRange controls, Value target, + UnitaryOpInterface rotation) { + const size_t numControls = controls.size(); + const auto [k1, k2] = partitionControls(numControls); + SmallVector wires(controls); + wires.push_back(target); + GateEmitter emitter(builder, loc, wires); + + const auto halfMcx = [&](size_t begin, size_t count) { + SmallVector map; + map.reserve(numControls + 1); + for (size_t control = begin; control < begin + count; ++control) { + map.push_back(control); + } + map.push_back(numControls); + // The balanced split provides at least count - 2 dirty helpers. Each + // exact MCX restores these opposite controls before the next rotation. + for (size_t control = 0; control < numControls; ++control) { + if (control < begin || control >= begin + count) { + map.push_back(control); + } + } + auto plan = planBorrowedHelperMcx(count); + for (auto& op : plan.ops) { + remapPlanOpInPlace(op, map); + } + return plan; + }; + const CircuitPlan firstHalf = halfMcx(0, k1); + const CircuitPlan secondHalf = halfMcx(k1, k2); + + auto quarter = + arith::MulFOp::create(builder, loc, rotation.getParameters()[0], + mqt::constantFromScalar(builder, loc, 0.25)); + auto negativeQuarter = arith::NegFOp::create(builder, loc, quarter); + const bool isY = isa(rotation.getOperation()); + const bool isX = isa(rotation.getOperation()); + const auto rotate = [&](Value angle) { + if (isY) { + emitter.ry(numControls, angle); + } else { + emitter.rz(numControls, angle); + } + }; + + // RX(theta) = H RZ(theta) H. In either remaining axis, the four rotations + // sum to theta exactly when both control halves are all ones, else to zero. + if (isX) { + emitter.h(numControls); + } + for (size_t repeat = 0; repeat < 2; ++repeat) { + lowerPlan(emitter, firstHalf); + rotate(negativeQuarter); + lowerPlan(emitter, secondHalf); + rotate(quarter); + } + if (isX) { + emitter.h(numControls); + } + return wires; +} + // Vale + Barenco-relative residual at this MCP width. static constexpr size_t K_MCP_VALE_RELATIVE_RESIDUAL_CONTROLS = 4; @@ -1246,7 +1131,7 @@ static CircuitPlan planMcpSp22(double theta, size_t numControls) { } // MCZ core: k=4 relative-phase C^4(Z); SP22 MCP(π) for 5..32; else HP24. -static CircuitPlan mczCoreForWidth(size_t numControls, size_t numWires) { +static CircuitPlan mczCoreForWidth(size_t numControls) { if (numControls == 4) { return planMczRelativePhaseK4(); } @@ -1254,7 +1139,7 @@ static CircuitPlan mczCoreForWidth(size_t numControls, size_t numWires) { numControls <= K_MCX_SP22_MAX_CONTROLS) { return planMcpSp22(K_PI, numControls); } - return planHp24Core(numWires, selectHp24Policy(numControls)); + return planHp24Core(numControls); } // General-angle MCP: SP22 at k >= 5, else C²P / Vale (relative residual at 4). @@ -1390,6 +1275,33 @@ struct DecomposeControlledGatePattern final : OpRewritePattern { if (op.getNumTargets() != 1) { return failure(); } + if (isa(inner.getOperation())) { + // Y = S X S†; the new MCX reuses this pass's width selection. + rewriter.setInsertionPoint(op); + auto loc = op.getLoc(); + auto target = + SdgOp::create(rewriter, loc, op.getInputTarget(0)).getOutputQubit(0); + auto mcx = CtrlOp::create( + rewriter, loc, op.getControlsIn(), target, [&](Value targetArg) { + return XOp::create(rewriter, loc, targetArg).getOutputQubit(0); + }); + SmallVector results(mcx.getOutputControls()); + results.push_back( + SOp::create(rewriter, loc, mcx.getOutputTarget(0)).getOutputQubit(0)); + rewriter.replaceOp(op, results); + return success(); + } + if (isa(inner.getOperation())) { + // Verified support operations cannot depend on the body's qubits. + // Hoist them so region-local symbolic angles survive the replacement. + mqt::hoistSupportingOpsBefore(*op.getBody(), inner.getOperation(), op, + rewriter); + rewriter.setInsertionPoint(op); + rewriter.replaceOp(op, synthesizeMultiControlledRotation( + rewriter, op.getLoc(), op.getControlsIn(), + op.getInputTarget(0), inner)); + return success(); + } const auto spec = matchControlledTarget(inner); if (!spec) { return failure(); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 68d9871060..49a2e21425 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -226,7 +226,8 @@ parseOutputFormat(const StringRef format) { static llvm::cl::opt enableDecomposeMultiControlled( "decompose-multi-controlled", llvm::cl::desc( - "Decompose controlled X/Z/phase/SWAP gates and qco.rccx that act on at " + "Decompose controlled X/Y/Z/rotation/phase/SWAP gates and qco.rccx " + "that act on at " "least --decompose-multi-controlled-min-qubits qubits (default 3)."), llvm::cl::init(false)); @@ -234,7 +235,8 @@ static llvm::cl::opt decomposeMultiControlledMinQubits( "decompose-multi-controlled-min-qubits", llvm::cl::desc( "Minimum qubit count for --decompose-multi-controlled: decompose " - "controlled X/Z/phase/SWAP gates and qco.rccx that act on at least " + "controlled X/Y/Z/rotation/phase/SWAP gates and qco.rccx that act on " + "at least " "this many qubits (default 3; must be at least 3). Higher values leave " "narrower gates undecomposed."), llvm::cl::init(3)); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index ec516326dc..cf51df6cd0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -17,25 +17,32 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/DDAdapter.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include +#include #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -50,7 +57,7 @@ using namespace mlir; using namespace mlir::qco; /// DD for k=2…20 plus the first HP24 width (k=33): full matrix DD through k=8 -/// (MCX/MCZ) or k=6 (MCP); basis-state DD for larger MCX/MCZ widths; +/// (MCX/MCY/MCZ) or k=6 (MCP); basis-state DD for larger Pauli widths; /// coherent-state DD at selected policy boundaries and representative larger /// MCP widths. static constexpr std::array K_DD_CONTROL_COUNTS = { @@ -58,8 +65,10 @@ static constexpr std::array K_DD_CONTROL_COUNTS = { }; static constexpr size_t K_MATRIX_DD_MAX_PAULI = 8; static constexpr size_t K_MATRIX_DD_MAX_MCP = 6; -static constexpr std::array K_COHERENT_HP24_CONTROL_COUNTS = { - 10, 11, 21, 22, 23, +/// Retain representative SP22 widths and cover the HP24 crossover, both +/// dirty-helper modes, and wide phase ladders that can fold to identity. +static constexpr std::array K_COHERENT_PAULI_CONTROL_COUNTS = { + 10, 11, 21, 22, 23, 32, 33, 34, 47, 48, 63, 64, }; static constexpr std::array K_COHERENT_MCP_CONTROL_COUNTS = {7, 12}; /// Additional fully-lowered/CX smoke checks for k > 20 through the SP22 MCX @@ -121,8 +130,21 @@ static constexpr std::array K_EXPECTED_MCX_CX = { return small[k]; } +/// CX regression budget for the borrowed-helper rotation construction. +/// Each half-MCX occurs twice and costs 1/6/14 CX at 1/2/3 controls. +/// Above that, its two passes each use a 6-CX CCX, a 3-CX RCCX, +/// and 2(n-3) two-CX gadgets: 8n-6 CX. Both halves reach this case at k=8. +[[nodiscard]] static constexpr size_t expectedMcrCxBudget(size_t k) { + if (k >= 8) { + return (16 * k) - 24; + } + constexpr std::array small = {0, 0, 4, 14, 24, 40, 56, 80}; + return small[k]; +} + namespace { -enum class ControlledPauli : uint8_t { X, Z }; +enum class ControlledPauli : uint8_t { X, Y, Z }; +enum class RotationAxis : uint8_t { X, Y, Z }; } // namespace [[nodiscard]] static dd::Controls makeControls(size_t numControls) { @@ -134,8 +156,9 @@ enum class ControlledPauli : uint8_t { X, Z }; } [[nodiscard]] static dd::GateMatrix pauliMatrix(ControlledPauli pauli) { - const auto matrix = pauli == ControlledPauli::X ? XOp::getUnitaryMatrix() - : ZOp::getUnitaryMatrix(); + const auto matrix = pauli == ControlledPauli::X ? XOp::getUnitaryMatrix() + : pauli == ControlledPauli::Y ? YOp::getUnitaryMatrix() + : ZOp::getUnitaryMatrix(); return {matrix(0, 0), matrix(0, 1), matrix(1, 0), matrix(1, 1)}; } @@ -170,18 +193,21 @@ class MultiControlledDecompositionTest : public testing::Test { std::unique_ptr context_; }; -class McxDdTest : public MultiControlledDecompositionTest, - public testing::WithParamInterface {}; -class MczDdTest : public MultiControlledDecompositionTest, - public testing::WithParamInterface {}; +class McPauliDdTest + : public MultiControlledDecompositionTest, + public testing::WithParamInterface> { +}; class McpDdTest : public MultiControlledDecompositionTest, public testing::WithParamInterface {}; -class McxSmokeTest : public MultiControlledDecompositionTest, - public testing::WithParamInterface {}; -class MczSmokeTest : public MultiControlledDecompositionTest, - public testing::WithParamInterface {}; +class McPauliSmokeTest + : public MultiControlledDecompositionTest, + public testing::WithParamInterface> { +}; class McpSmokeTest : public MultiControlledDecompositionTest, public testing::WithParamInterface {}; +class McrDdTest + : public MultiControlledDecompositionTest, + public testing::WithParamInterface> {}; } // namespace @@ -199,6 +225,8 @@ buildControlledPauliModule(MLIRContext* context, size_t numControls, auto target = wires.back(); if (pauli == ControlledPauli::X) { b.mcx(controls, target); + } else if (pauli == ControlledPauli::Y) { + b.mcy(controls, target); } else { b.mcz(controls, target); } @@ -206,16 +234,6 @@ buildControlledPauliModule(MLIRContext* context, size_t numControls, }); } -[[nodiscard]] static OwningOpRef buildMcxModule(MLIRContext* context, - size_t numControls) { - return buildControlledPauliModule(context, numControls, ControlledPauli::X); -} - -[[nodiscard]] static OwningOpRef buildMczModule(MLIRContext* context, - size_t numControls) { - return buildControlledPauliModule(context, numControls, ControlledPauli::Z); -} - [[nodiscard]] static OwningOpRef buildMcpModule(MLIRContext* context, size_t numControls, double theta) { return QCOProgramBuilder::build( @@ -230,6 +248,81 @@ buildMcpModule(MLIRContext* context, size_t numControls, double theta) { }); } +[[nodiscard]] static Value applyRotation(QCOProgramBuilder& builder, + RotationAxis axis, Value theta, + Value target) { + if (axis == RotationAxis::X) { + return builder.rx(theta, target); + } + if (axis == RotationAxis::Y) { + return builder.ry(theta, target); + } + return builder.rz(theta, target); +} + +static void buildControlledRotation(QCOProgramBuilder& builder, + size_t numControls, RotationAxis axis, + Value theta, bool regionLocal = false) { + SmallVector wires; + for (size_t i = 0; i <= numControls; ++i) { + wires.push_back(builder.staticQubit(i)); + } + const size_t target = numControls / 2; + SmallVector controls; + for (size_t i = numControls + 1; i-- > 0;) { + if (i != target) { + controls.push_back(wires[i]); + } + } + builder.ctrl(controls, wires[target], [&](Value targetArg) { + auto angle = + regionLocal ? arith::NegFOp::create(builder, theta).getResult() : theta; + return applyRotation(builder, axis, angle, targetArg); + }); +} + +[[nodiscard]] static OwningOpRef +buildMcrModule(MLIRContext* context, size_t numControls, RotationAxis axis, + double theta, bool runtimeAngle = false) { + Value parameter; + auto moduleOp = + QCOProgramBuilder::build(context, [&](QCOProgramBuilder& builder) { + parameter = builder.floatConstant(theta); + buildControlledRotation(builder, numControls, axis, parameter); + return SmallVector{}; + }); + if (moduleOp && runtimeAngle) { + auto funcOp = *moduleOp->getBody()->getOps().begin(); + funcOp.insertArgument(0, Float64Type::get(context), {}, funcOp.getLoc()); + parameter.replaceAllUsesWith(funcOp.getArgument(0)); + } + return moduleOp; +} + +// R_a(theta) = cos(theta/2) I - i sin(theta/2) sigma_a. +[[nodiscard]] static dd::GateMatrix rotationMatrix(RotationAxis axis, + double theta) { + const double cosine = std::cos(theta / 2); + const double sine = std::sin(theta / 2); + if (axis == RotationAxis::X) { + return { + cosine, + std::complex{0, -sine}, + std::complex{0, -sine}, + cosine, + }; + } + if (axis == RotationAxis::Y) { + return {cosine, -sine, sine, cosine}; + } + return { + std::complex{cosine, -sine}, + 0, + 0, + std::complex{cosine, sine}, + }; +} + [[nodiscard]] static OwningOpRef buildRCCXModule(MLIRContext* context) { return QCOProgramBuilder::build(context, [](QCOProgramBuilder& b) { @@ -247,6 +340,28 @@ buildRCCXModule(MLIRContext* context) { return numQubits; } +static void expectImplementsControlledRotation( + func::FuncOp funcOp, size_t numControls, RotationAxis axis, double theta, + const DDArgumentBindings& bindings = DDArgumentBindings()) { + ASSERT_EQ(countStaticQubits(funcOp), numControls + 1); + const auto package = std::make_unique(numControls + 1); + const auto actual = buildFunctionality(funcOp, *package, bindings); + ASSERT_TRUE(succeeded(actual)); + const auto target = static_cast(numControls / 2); + dd::Controls controls; + for (size_t i = 0; i <= numControls; ++i) { + if (i != static_cast(target)) { + controls.emplace(static_cast(i)); + } + } + const auto expected = + package->makeGateDD(rotationMatrix(axis, theta), controls, target); + // Full operator equality preserves phase and restores every borrowed control, + // including when controls are entangled with other qubits. + EXPECT_EQ(*actual, expected); + package->decRef(*actual); +} + static void expectFullyDecomposed(func::FuncOp funcOp) { funcOp.walk([](CtrlOp op) { EXPECT_EQ(op.getNumControls(), 1U); @@ -272,6 +387,21 @@ static void expectImplementsControlledPauli(func::FuncOp funcOp, dd->decRef(*decomposedDD); } +/// Compare the complete states, including phase, without requiring identical +/// DD nodes after floating-point synthesis. +static void expectStatesNear(dd::Package& package, const dd::VectorDD& actual, + const dd::VectorDD& expected) { + auto negativeExpected = expected; + negativeExpected.w = package.cn.lookup(-dd::RealNumber::val(expected.w.r), + -dd::RealNumber::val(expected.w.i)); + const auto difference = package.add(actual, negativeExpected); + package.incRef(difference); + constexpr double tolerance = 1e-11; + EXPECT_LE(package.innerProduct(difference, difference).r, + tolerance * tolerance); + package.decRef(difference); +} + static void expectMatchesReferenceOnBasisStates(func::FuncOp funcOp, size_t numControls, ControlledPauli pauli) { @@ -301,11 +431,7 @@ static void expectMatchesReferenceOnBasisStates(func::FuncOp funcOp, ASSERT_TRUE(succeeded(decomposedOutput)); const auto referenceOutput = dd->applyOperation( referenceGate, dd::makeBasisState(numQubits, basisState, *dd)); - EXPECT_EQ(decomposedOutput->p, referenceOutput.p); - EXPECT_NEAR(dd::RealNumber::val(decomposedOutput->w.r), - dd::RealNumber::val(referenceOutput.w.r), 1e-11); - EXPECT_NEAR(dd::RealNumber::val(decomposedOutput->w.i), - dd::RealNumber::val(referenceOutput.w.i), 1e-11); + expectStatesNear(*dd, *decomposedOutput, referenceOutput); dd->decRef(*decomposedOutput); dd->decRef(referenceOutput); } @@ -327,6 +453,12 @@ static void expectMatchesReferenceOnCoherentState(func::FuncOp funcOp, size_t numControls, bool targetOne, const dd::GateMatrix& referenceMatrix) { + /// Resolve small SP22 ladder phases before comparing the whole-state error. + const auto previousTolerance = dd::RealNumber::eps; + const auto restoreTolerance = llvm::make_scope_exit([previousTolerance] { + dd::ComplexNumbers::setTolerance(previousTolerance); + }); + dd::ComplexNumbers::setTolerance(1e-15); const auto numQubits = countStaticQubits(funcOp); ASSERT_EQ(numQubits, numControls + 1); expectFullyDecomposed(funcOp); @@ -340,11 +472,7 @@ expectMatchesReferenceOnCoherentState(func::FuncOp funcOp, size_t numControls, makeControlledGateDD(*dd, numControls, referenceMatrix), makeCoherentControlInput(numControls, targetOne, *dd)); - EXPECT_EQ(decomposedOutput->p, referenceOutput.p); - EXPECT_NEAR(dd::RealNumber::val(decomposedOutput->w.r), - dd::RealNumber::val(referenceOutput.w.r), 1e-11); - EXPECT_NEAR(dd::RealNumber::val(decomposedOutput->w.i), - dd::RealNumber::val(referenceOutput.w.i), 1e-11); + expectStatesNear(*dd, *decomposedOutput, referenceOutput); dd->decRef(*decomposedOutput); dd->decRef(referenceOutput); @@ -444,48 +572,204 @@ static void expectFullyLowered(ModuleOp moduleOp) { static LogicalResult runDecomposeMultiControlled( ModuleOp moduleOp, const DecomposeMultiControlledOptions& options = {}) { + if (failed(verify(moduleOp)) || failed(verifyLinearity(moduleOp))) { + return failure(); + } PassManager pm(moduleOp.getContext()); + pm.enableVerifier(); pm.addPass(createDecomposeMultiControlled(options)); - return pm.run(moduleOp); + if (failed(pm.run(moduleOp))) { + return failure(); + } + // The pass manager already verifies the output IR. + return verifyLinearity(moduleOp); } //===----------------------------------------------------------------------===// -// MCX / MCZ / MCP: DD + CX for k = 2..20 +// Multi-controlled rotations //===----------------------------------------------------------------------===// -TEST_P(McxDdTest, EquivalenceAndCxCount) { - const size_t k = GetParam(); - auto moduleOp = buildMcxModule(context(), k); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get()).succeeded()); - expectFullyLowered(moduleOp.get()); - EXPECT_EQ(countElementaryCxOps(moduleOp.get()), K_EXPECTED_MCX_CX[k]) - << "k=" << k; +TEST_P(McrDdTest, PreservesFullOperatorAndBorrowedControls) { + const auto [axis, numControls] = GetParam(); + for (const double theta : {0.0, 0.73, -1.21, 2 * std::numbers::pi}) { + SCOPED_TRACE(testing::Message() << "theta=" << theta); + auto moduleOp = buildMcrModule(context(), numControls, axis, theta); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(runDecomposeMultiControlled(moduleOp.get()))); + expectFullyLowered(moduleOp.get()); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectFullyDecomposed(funcOp); + expectImplementsControlledRotation(funcOp, numControls, axis, theta); + } +} - auto funcOp = *moduleOp->getBody()->getOps().begin(); - if (k <= K_MATRIX_DD_MAX_PAULI) { - expectImplementsControlledPauli(funcOp, k, ControlledPauli::X); - } else { - expectMatchesReferenceOnBasisStates(funcOp, k, ControlledPauli::X); +INSTANTIATE_TEST_SUITE_P( + DdRange, McrDdTest, + testing::Combine(testing::Values(RotationAxis::X, RotationAxis::Y, + RotationAxis::Z), + testing::Values(2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U)), + ([](const testing::TestParamInfo>& info) { + const auto [axis, numControls] = info.param; + return std::string(axis == RotationAxis::X ? "Rx" + : axis == RotationAxis::Y ? "Ry" + : "Rz") + + "k" + std::to_string(numControls); + })); + +TEST_F(MultiControlledDecompositionTest, RotationsPreserveRuntimeAngles) { + constexpr size_t numControls = 8; + for (const auto axis : {RotationAxis::X, RotationAxis::Y, RotationAxis::Z}) { + for (const bool regionLocal : {false, true}) { + SCOPED_TRACE(testing::Message() << "axis=" << static_cast(axis) + << " regionLocal=" << regionLocal); + Value parameter; + auto moduleOp = + QCOProgramBuilder::build(context(), [&](QCOProgramBuilder& builder) { + parameter = builder.floatConstant(0.73); + buildControlledRotation(builder, numControls, axis, parameter, + regionLocal); + return SmallVector{}; + }); + ASSERT_TRUE(moduleOp); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + funcOp.insertArgument(0, Float64Type::get(context()), {}, + funcOp.getLoc()); + parameter.replaceAllUsesWith(funcOp.getArgument(0)); + ASSERT_TRUE(succeeded(runDecomposeMultiControlled(moduleOp.get()))); + expectFullyLowered(moduleOp.get()); + expectFullyDecomposed(funcOp); + for (const double theta : {-0.91, 2 * std::numbers::pi}) { + const DDArgumentBindings bindings{ + { + funcOp.getArgument(0), + FloatAttr::get(Float64Type::get(context()), theta), + }, + }; + expectImplementsControlledRotation( + funcOp, numControls, axis, regionLocal ? -theta : theta, bindings); + } + } } } -TEST_P(MczDdTest, EquivalenceAndCxCount) { - const size_t k = GetParam(); - auto moduleOp = buildMczModule(context(), k); +TEST_F(MultiControlledDecompositionTest, + RotationsUseLinearResourcesWithoutExtraQubits) { + for (const auto axis : {RotationAxis::X, RotationAxis::Y, RotationAxis::Z}) { + for (const size_t numControls : { + 2U, + 3U, + 4U, + 5U, + 6U, + 7U, + 8U, + 9U, + 15U, + 16U, + 17U, + 31U, + 32U, + 33U, + 63U, + 64U, + }) { + for (const bool runtimeAngle : {false, true}) { + SCOPED_TRACE(testing::Message() + << "axis=" << static_cast(axis) << " controls=" + << numControls << " runtimeAngle=" << runtimeAngle); + auto moduleOp = + buildMcrModule(context(), numControls, axis, 0.73, runtimeAngle); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(runDecomposeMultiControlled(moduleOp.get()))); + expectFullyLowered(moduleOp.get()); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectFullyDecomposed(funcOp); + EXPECT_EQ(countStaticQubits(funcOp), numControls + 1); + funcOp.walk( + [](AllocOp) { ADD_FAILURE() << "unexpected helper qubit"; }); + // The same bound covers every axis and permits future cancellation. + EXPECT_LE(countElementaryCxOps(moduleOp.get()), + expectedMcrCxBudget(numControls)); + size_t elementaryGates = 0; + funcOp.walk([&](UnitaryOpInterface op) { + if (op->getNumRegions() == 0) { + ++elementaryGates; + } + }); + EXPECT_LE(elementaryGates, 60 * numControls); + } + } + } +} + +TEST_F(MultiControlledDecompositionTest, RotationsRespectMinQubits) { + for (const auto axis : {RotationAxis::X, RotationAxis::Y, RotationAxis::Z}) { + auto moduleOp = buildMcrModule(context(), 3, axis, 0.73); + ASSERT_TRUE(moduleOp); + DecomposeMultiControlledOptions options; + options.minQubits = 5; + ASSERT_TRUE( + succeeded(runDecomposeMultiControlled(moduleOp.get(), options))); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectImplementsControlledRotation(funcOp, 3, axis, 0.73); + + options.minQubits = 4; + ASSERT_TRUE( + succeeded(runDecomposeMultiControlled(moduleOp.get(), options))); + EXPECT_EQ(countMultiControlledOps(moduleOp.get(), 3), 0U); + expectImplementsControlledRotation(funcOp, 3, axis, 0.73); + } +} + +TEST_F(MultiControlledDecompositionTest, PreservesTargetNativeRotations) { + using TargetOperation = CompilerTarget::Operation; + const std::vector operations{ + llvm::cantFail(TargetOperation::create( + "rx", TargetOperation::Arity::variadic(4), 1)), + llvm::cantFail(TargetOperation::create( + "ry", TargetOperation::Arity::variadic(4), 1)), + llvm::cantFail(TargetOperation::create( + "rz", TargetOperation::Arity::variadic(4), 1)), + }; + const auto target = llvm::cantFail(CompilerTarget::create( + 4, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations(operations))); + for (const auto axis : {RotationAxis::X, RotationAxis::Y, RotationAxis::Z}) { + auto moduleOp = buildMcrModule(context(), 3, axis, 0.73); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(moduleOp.get()))); + ASSERT_TRUE(succeeded(verifyLinearity(moduleOp.get()))); + PassManager pm(context()); + pm.addPass(createDecomposeMultiControlled(target)); + ASSERT_TRUE(succeeded(pm.run(moduleOp.get()))); + ASSERT_TRUE(succeeded(verify(moduleOp.get()))); + ASSERT_TRUE(succeeded(verifyLinearity(moduleOp.get()))); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectImplementsControlledRotation(funcOp, 3, axis, 0.73); + } +} + +//===----------------------------------------------------------------------===// +// MCX / MCY / MCZ / MCP: DD + CX for k = 2..20 and k = 33 +//===----------------------------------------------------------------------===// + +TEST_P(McPauliDdTest, EquivalenceAndCxCount) { + const auto [pauli, k] = GetParam(); + auto moduleOp = buildControlledPauliModule(context(), k, pauli); ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get()).succeeded()); expectFullyLowered(moduleOp.get()); - // MCZ shares the MCX elementary sequences / cores (no extra CX from the - // outer H sandwich on X). + // MCY and MCZ share MCX's CX budget; their basis changes add no CX. EXPECT_EQ(countElementaryCxOps(moduleOp.get()), K_EXPECTED_MCX_CX[k]) << "k=" << k; auto funcOp = *moduleOp->getBody()->getOps().begin(); if (k <= K_MATRIX_DD_MAX_PAULI) { - expectImplementsControlledPauli(funcOp, k, ControlledPauli::Z); + expectImplementsControlledPauli(funcOp, k, pauli); } else { - expectMatchesReferenceOnBasisStates(funcOp, k, ControlledPauli::Z); + expectMatchesReferenceOnBasisStates(funcOp, k, pauli); } } @@ -504,16 +788,21 @@ TEST_P(McpDdTest, EquivalenceAndCxCount) { } } -INSTANTIATE_TEST_SUITE_P(DdRange, McxDdTest, - testing::ValuesIn(K_DD_CONTROL_COUNTS), - [](const testing::TestParamInfo& info) { - return "k" + std::to_string(info.param); - }); -INSTANTIATE_TEST_SUITE_P(DdRange, MczDdTest, - testing::ValuesIn(K_DD_CONTROL_COUNTS), - [](const testing::TestParamInfo& info) { - return "k" + std::to_string(info.param); - }); +static std::string pauliTestName( + const testing::TestParamInfo>& info) { + const auto [pauli, k] = info.param; + return std::string(pauli == ControlledPauli::X ? "X" + : pauli == ControlledPauli::Y ? "Y" + : "Z") + + "k" + std::to_string(k); +} + +INSTANTIATE_TEST_SUITE_P( + DdRange, McPauliDdTest, + testing::Combine(testing::Values(ControlledPauli::X, ControlledPauli::Y, + ControlledPauli::Z), + testing::ValuesIn(K_DD_CONTROL_COUNTS)), + pauliTestName); INSTANTIATE_TEST_SUITE_P(DdRange, McpDdTest, testing::ValuesIn(K_DD_CONTROL_COUNTS), [](const testing::TestParamInfo& info) { @@ -521,12 +810,12 @@ INSTANTIATE_TEST_SUITE_P(DdRange, McpDdTest, }); TEST_F(MultiControlledDecompositionTest, - CoherentStatesMatchAcrossHp24PolicyBoundaries) { - for (const auto k : K_COHERENT_HP24_CONTROL_COUNTS) { - for (const auto pauli : {ControlledPauli::X, ControlledPauli::Z}) { + CoherentStatesMatchAcrossSynthesisBoundaries) { + for (const auto k : K_COHERENT_PAULI_CONTROL_COUNTS) { + for (const auto pauli : + {ControlledPauli::X, ControlledPauli::Y, ControlledPauli::Z}) { SCOPED_TRACE(testing::Message() - << "k=" << k - << " pauli=" << (pauli == ControlledPauli::X ? "X" : "Z")); + << "k=" << k << " pauli=" << static_cast(pauli)); auto moduleOp = buildControlledPauliModule(context(), k, pauli); ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get()).succeeded()); @@ -552,19 +841,9 @@ TEST_F(MultiControlledDecompositionTest, CoherentStatesMatchForLargerSp22Mcp) { // Additional smoke checks for k > 20 — fully lowered, pinned CX //===----------------------------------------------------------------------===// -TEST_P(McxSmokeTest, FullyLowersWithExpectedCx) { - const size_t k = GetParam(); - auto moduleOp = buildMcxModule(context(), k); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get()).succeeded()); - expectFullyLowered(moduleOp.get()); - EXPECT_EQ(countElementaryCxOps(moduleOp.get()), K_EXPECTED_MCX_CX[k]) - << "k=" << k; -} - -TEST_P(MczSmokeTest, FullyLowersWithExpectedCx) { - const size_t k = GetParam(); - auto moduleOp = buildMczModule(context(), k); +TEST_P(McPauliSmokeTest, FullyLowersWithExpectedCx) { + const auto [pauli, k] = GetParam(); + auto moduleOp = buildControlledPauliModule(context(), k, pauli); ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get()).succeeded()); expectFullyLowered(moduleOp.get()); @@ -582,16 +861,12 @@ TEST_P(McpSmokeTest, FullyLowersWithExpectedCx) { EXPECT_EQ(countEffectiveCxOps(moduleOp.get()), expectedMcpCx(k)) << "k=" << k; } -INSTANTIATE_TEST_SUITE_P(SmokeRange, McxSmokeTest, - testing::ValuesIn(K_SMOKE_CONTROL_COUNTS), - [](const testing::TestParamInfo& info) { - return "k" + std::to_string(info.param); - }); -INSTANTIATE_TEST_SUITE_P(SmokeRange, MczSmokeTest, - testing::ValuesIn(K_SMOKE_CONTROL_COUNTS), - [](const testing::TestParamInfo& info) { - return "k" + std::to_string(info.param); - }); +INSTANTIATE_TEST_SUITE_P( + SmokeRange, McPauliSmokeTest, + testing::Combine(testing::Values(ControlledPauli::X, ControlledPauli::Y, + ControlledPauli::Z), + testing::ValuesIn(K_SMOKE_CONTROL_COUNTS)), + pauliTestName); INSTANTIATE_TEST_SUITE_P(SmokeRange, McpSmokeTest, testing::ValuesIn(K_SMOKE_CONTROL_COUNTS), [](const testing::TestParamInfo& info) { @@ -607,6 +882,10 @@ TEST_F(MultiControlledDecompositionTest, LeavesSingleControlledUntouched) { QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { builder.cx(builder.staticQubit(0), builder.staticQubit(1)); builder.cz(builder.staticQubit(2), builder.staticQubit(3)); + builder.crx(0.73, builder.staticQubit(4), builder.staticQubit(5)); + builder.cry(0.73, builder.staticQubit(6), builder.staticQubit(7)); + builder.crz(0.73, builder.staticQubit(8), builder.staticQubit(9)); + builder.cy(builder.staticQubit(10), builder.staticQubit(11)); return SmallVector{}; }); ASSERT_TRUE(moduleOp); @@ -618,7 +897,7 @@ TEST_F(MultiControlledDecompositionTest, LeavesSingleControlledUntouched) { ++singleControlled; } }); - EXPECT_EQ(singleControlled, 2U); + EXPECT_EQ(singleControlled, 6U); } TEST_F(MultiControlledDecompositionTest, DecomposesRCCX) { @@ -672,16 +951,53 @@ TEST_F(MultiControlledDecompositionTest, } TEST_F(MultiControlledDecompositionTest, MinQubitsThreshold) { - auto moduleOp = buildMcxModule(context(), 2); - ASSERT_TRUE(moduleOp); - DecomposeMultiControlledOptions options; - options.minQubits = 4; - ASSERT_TRUE(runDecomposeMultiControlled(moduleOp.get(), options).succeeded()); - EXPECT_EQ(countMultiControlledOps(moduleOp.get(), 2), 1U); + for (const auto pauli : + {ControlledPauli::X, ControlledPauli::Y, ControlledPauli::Z}) { + SCOPED_TRACE(testing::Message() + << "pauli=" << static_cast(pauli)); + auto moduleOp = buildControlledPauliModule(context(), 2, pauli); + ASSERT_TRUE(moduleOp); + DecomposeMultiControlledOptions options; + options.minQubits = 4; + ASSERT_TRUE( + runDecomposeMultiControlled(moduleOp.get(), options).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get(), 2), 1U); + + options.minQubits = 3; + ASSERT_TRUE( + runDecomposeMultiControlled(moduleOp.get(), options).succeeded()); + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectImplementsControlledPauli(funcOp, 2, pauli); - options.minQubits = 2; - EXPECT_FALSE( - runDecomposeMultiControlled(moduleOp.get(), options).succeeded()); + options.minQubits = 2; + EXPECT_FALSE( + runDecomposeMultiControlled(moduleOp.get(), options).succeeded()); + } +} + +TEST_F(MultiControlledDecompositionTest, PreservesTargetNativeMcy) { + auto moduleOp = buildControlledPauliModule(context(), 3, ControlledPauli::Y); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(moduleOp.get()))); + ASSERT_TRUE(succeeded(verifyLinearity(moduleOp.get()))); + using TargetOperation = CompilerTarget::Operation; + const std::vector operations{ + llvm::cantFail( + TargetOperation::create("y", TargetOperation::Arity::variadic(4), 0)), + }; + const auto target = llvm::cantFail(CompilerTarget::create( + 4, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations(operations))); + PassManager pm(context()); + pm.addPass(createDecomposeMultiControlled(target)); + ASSERT_TRUE(succeeded(pm.run(moduleOp.get()))); + EXPECT_TRUE(succeeded(verify(moduleOp.get()))); + EXPECT_TRUE(succeeded(verifyLinearity(moduleOp.get()))); + EXPECT_EQ(countMultiControlledOps(moduleOp.get(), 3), 1U); + moduleOp->walk([](CtrlOp op) { + ASSERT_EQ(op.getNumBodyUnitaries(), 1U); + EXPECT_TRUE(isa(op.getBodyUnitary(0).getOperation())); + }); } TEST_F(MultiControlledDecompositionTest, DecomposesSingleControlledSwap) { diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index e608caaa80..33cce08f6e 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -631,7 +631,7 @@ class QCOProgram(Program): """Prepare the program for qubit reuse and reuse eligible qubits.""" def decompose_multi_controlled(self, *, min_qubits: int = 3) -> None: - """Decompose controlled X/Z/SWAP gates, qco.rccx, and constant-angle phase gates that act on at least min_qubits qubits (min_qubits must be at least 3; default 3 means wider than two-qubit).""" + """Decompose controlled X/Y/Z/SWAP and RX/RY/RZ gates, qco.rccx, and constant-angle phase gates that act on at least min_qubits qubits (min_qubits must be at least 3; default 3 means wider than two-qubit).""" def compile_for_target( self, target_environment: TargetEnvironment, *, enable_timing: bool = False, enable_statistics: bool = False diff --git a/test/bench/compare_controlled_rotations.py b/test/bench/compare_controlled_rotations.py new file mode 100644 index 0000000000..3a7c49b35c --- /dev/null +++ b/test/bench/compare_controlled_rotations.py @@ -0,0 +1,160 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Compare ancilla-free Core/Qiskit rotation synthesis in a common u,cx basis. + +Run with the locally built package, for example:: + + uv run --no-sync python test/bench/compare_controlled_rotations.py \ + --output build/bench/controlled-rotations.csv + +Timing excludes input preparation, import/export, basis conversion, and routing. +Both outputs receive the same basis conversion and level-3 optimization, with +no assumption that input qubits start in zero. Small cases also check the +phase-sensitive operator, including symbolic binding at 2*pi. +""" + +# Standalone benchmark executable; this directory is not a Python package. +# ruff: file-ignore[implicit-namespace-package] + +from __future__ import annotations + +import argparse +import csv +import hashlib +import logging +from importlib.metadata import version +from math import pi +from pathlib import Path +from statistics import median +from time import perf_counter_ns + +import numpy as np +from qiskit import QuantumCircuit, transpile +from qiskit.circuit import AnnotatedOperation, ControlModifier, Parameter +from qiskit.circuit.library import RXGate, RYGate, RZGate +from qiskit.quantum_info import Operator + +from mqt.core.mlir import QCOProgram, QCProgram + +LOGGER = logging.getLogger(__name__) + + +def synthesize_core(source: str, samples: int) -> tuple[QuantumCircuit, float]: + """Measure decomposition of fresh copies, excluding one warmup. + + Returns: + The synthesized circuit and median synthesis time in milliseconds. + """ + timings = [] + for _ in range(samples + 1): + program = QCOProgram.from_mlir_str(source) + start = perf_counter_ns() + program.decompose_multi_controlled() + timings.append((perf_counter_ns() - start) / 1e6) + return program.to_qc().to_qiskit(), median(timings[1:]) + + +def synthesize_qiskit(axis: str, controls: int, angle: float | Parameter, samples: int) -> tuple[QuantumCircuit, float]: + """Measure the public no-ancilla synthesis methods, excluding one warmup. + + Returns: + The synthesized circuit and median synthesis time in milliseconds. + """ + timings = [] + for _ in range(samples + 1): + circuit = QuantumCircuit(controls + 1) + start = perf_counter_ns() + if axis == "ry": + circuit.mcry(angle, list(range(controls)), controls, mode="noancilla") + else: + getattr(circuit, f"mc{axis}")(angle, list(range(controls)), controls) + timings.append((perf_counter_ns() - start) / 1e6) + return circuit, median(timings[1:]) + + +def metrics(circuit: QuantumCircuit, reference: QuantumCircuit) -> dict[str, int | float | str]: + """Measure common-basis circuits and verify phase on small operators. + + Returns: + Gate counts, depths, and the phase-sensitive error for small operators. + """ + assert circuit.num_qubits == reference.num_qubits + normalized = transpile( + circuit, basis_gates=["u", "cx"], optimization_level=0, seed_transpiler=0, qubits_initially_zero=False + ) + optimized = transpile( + normalized, basis_gates=["u", "cx"], optimization_level=3, seed_transpiler=0, qubits_initially_zero=False + ) + result: dict[str, int | float | str] = {} + for prefix, output in (("raw", normalized), ("optimized", optimized)): + assert set(output.count_ops()) <= {"u", "cx"} + result[f"{prefix}_cx"] = output.count_ops().get("cx", 0) + result[f"{prefix}_one_qubit"] = output.count_ops().get("u", 0) + result[f"{prefix}_depth"] = output.depth() + result[f"{prefix}_cx_depth"] = output.depth(lambda instruction: instruction.operation.num_qubits == 2) + result["max_operator_error"] = "" + if reference.num_qubits <= 6: + error = 0.0 + for angle in (-0.61, 2 * pi) if reference.parameters else (0.73,): + expected = reference.assign_parameters(dict.fromkeys(reference.parameters, angle)) + for output in (normalized, optimized): + actual = output.assign_parameters(dict.fromkeys(output.parameters, angle)) + error = max(error, float(np.max(np.abs(Operator(actual).data - Operator(expected).data)))) + assert error <= 1e-10, f"phase-sensitive operator error: {error}" + result["max_operator_error"] = error + return result + + +def main() -> None: + """Write quality and median synthesis times for numeric and symbolic gates.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--controls", nargs="+", type=int, default=[2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 32, 64]) + parser.add_argument("--samples", type=int, default=9) + parser.add_argument("--output", type=Path, default=Path("build/bench/controlled-rotations.csv")) + args = parser.parse_args() + if args.samples < 1 or any(count < 2 for count in args.controls): + parser.error("samples must be positive and control counts must be at least two") + logging.basicConfig(level=logging.WARNING, format="%(message)s") + LOGGER.setLevel(logging.INFO) + source = Path(__file__).resolve().parents[2] / ( + "mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp" + ) + LOGGER.info("Core %s; Qiskit %s; samples=%s", version("mqt-core"), version("qiskit"), args.samples) + LOGGER.info("DecomposeMultiControlled.cpp SHA256: %s", hashlib.sha256(source.read_bytes()).hexdigest()) + rows = [] + for axis, gate in (("rx", RXGate), ("ry", RYGate), ("rz", RZGate)): + for controls in args.controls: + for kind, angle in (("numeric", 0.73), ("symbolic", Parameter("theta"))): + reference = QuantumCircuit(controls + 1) + reference.append(AnnotatedOperation(gate(angle), ControlModifier(controls)), reference.qubits) + source_ir = QCProgram.from_qiskit(reference).to_qco().ir + for backend in ("core", "qiskit"): + circuit, milliseconds = ( + synthesize_core(source_ir, args.samples) + if backend == "core" + else synthesize_qiskit(axis, controls, angle, args.samples) + ) + rows.append({ + "axis": axis, + "controls": controls, + "angle": kind, + "backend": backend, + "synthesis_ms": milliseconds, + **metrics(circuit, reference), + }) + LOGGER.info("%s, controls=%s, %s", axis, controls, kind) + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("w", newline="", encoding="utf-8") as output: + writer = csv.DictWriter(output, fieldnames=list(rows[0]), lineterminator="\n") + writer.writeheader() + writer.writerows(rows) + + +if __name__ == "__main__": + main() diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 1f5720d471..f96434634f 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -895,10 +895,11 @@ def test_typed_programs_normalize_global_phases() -> None: assert qco.ir == once -def test_qco_program_decomposes_multi_controlled() -> None: +@pytest.mark.parametrize("gate", ["x", "y", "rx(0.73)", "ry(0.73)", "rz(0.73)"]) +def test_qco_program_decomposes_multi_controlled(gate: str) -> None: """Decompose multi-controlled gates through the typed QCOProgram API.""" qco = compile_program( - 'OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ x q[0], q[1], q[2];', + f'OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ {gate} q[0], q[1], q[2];', output=OutputFormat.QCO, ) assert isinstance(qco, QCOProgram) diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 2184fd1bf1..c1c41acbae 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -262,6 +262,23 @@ def test_two_qubit_dense_unitary_compiles_to_target_basis() -> None: assert set(restored.count_ops()) <= {"u", "cx"} +def test_symbolic_multi_controlled_rotations_decompose_and_bind() -> None: + """Export and bind angle expressions produced by decomposition.""" + theta = Parameter("theta") + circuit = QuantumCircuit(3) + circuit.append(AnnotatedOperation(library.RYGate(theta), ControlModifier(2)), circuit.qubits) + program = QCProgram.from_qiskit(circuit).to_qco() + + program.decompose_multi_controlled() + restored = program.to_qc().to_qiskit() + + assert {parameter.name for parameter in restored.parameters} == {theta.name} + assert all(item.operation.num_qubits <= 2 for item in restored.data) + expected = circuit.assign_parameters({theta: -0.61}) + actual = _assign_parameter_values(restored, {theta.name: -0.61}) + assert np.allclose(Operator(actual).data, Operator(expected).data, atol=1e-10, rtol=0) + + def test_controlled_dense_unitary_export_preserves_operation_order() -> None: """Export a controlled dense matrix with a Qiskit control annotation.""" program = QCProgram.from_mlir_str(