Skip to content

Commit b23898f

Browse files
chloe-quantinuumsjdilkescqc-alecyao-cqc
authored
feat: Add T-count optimization pass (#2049)
Adds a t optimization pass that applies todd with a potential ancilla budget to pg libs --------- Co-authored-by: Silas Dilkes <36165522+sjdilkes@users.noreply.github.com> Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> Co-authored-by: yao-cqc <yao.tang@cambridgequantum.com> Co-authored-by: Alec Edgington <alec.edgington@quantinuum.com>
1 parent 3c1a217 commit b23898f

23 files changed

Lines changed: 2371 additions & 29 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ jobs:
199199
run: uv sync --only-group pg-libs --python ${{ env.PYTHON_HIGHEST }}
200200
- name: Run unstable_simd tests
201201
working-directory: pg-libs
202-
run: uv run --only-group pg-libs cargo +${{ steps.nightly-toolchain.outputs.channel }} test -p tk-pg-bitpacked -p tk-pg-qm-tableau -p tk-pg-greedy-synth --features unstable_simd
202+
run: uv run --only-group pg-libs cargo +${{ steps.nightly-toolchain.outputs.channel }} test -p tk-pg-bitpacked -p tk-pg-qm-tableau -p tk-pg-greedy-synth -p tk-pg-t-optimize --features unstable_simd
203203

204204
benches:
205205
name: Continuous benchmarking 🏋️

‎Cargo.lock‎

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ members = [
1919
"pg-libs/pg-utils/tk-pg-utils",
2020
"pg-libs/pg-passes/tk-pg-canonical-form",
2121
"pg-libs/pg-passes/tk-pg-optimize",
22+
"pg-libs/pg-passes/tk-pg-t-optimize",
2223
"pg-libs/pg-passes/tk-pg-greedy-synth",
2324
"pg-libs/pg-passes/tk-pg-rebase",
2425
"pg-libs/tk-pg-qm-tableau",
@@ -125,6 +126,7 @@ pg-core = { package = "tk-pg-core", path = "pg-libs/tk-pg-core", version = "0.1.
125126
pg-canonical-form = { package = "tk-pg-canonical-form", path = "pg-libs/pg-passes/tk-pg-canonical-form", version = "0.1.0" }
126127
pg-greedy-synth = { package = "tk-pg-greedy-synth", path = "pg-libs/pg-passes/tk-pg-greedy-synth", version = "0.1.0" }
127128
pg-optimise = { package = "tk-pg-optimize", path = "pg-libs/pg-passes/tk-pg-optimize", version = "0.1.0" }
129+
pg-t-optimize = { package = "tk-pg-t-optimize", path = "pg-libs/pg-passes/tk-pg-t-optimize", version = "0.1.0" }
128130
pg-rebase = { package = "tk-pg-rebase", path = "pg-libs/pg-passes/tk-pg-rebase", version = "0.1.0" }
129131

130132
[profile.release.package.tket-py]
@@ -146,6 +148,8 @@ opt-level = 1
146148
opt-level = 1
147149
[profile.test.package.tk-pg-optimize]
148150
opt-level = 1
151+
[profile.test.package.tk-pg-t-optimize]
152+
opt-level = 1
149153
[profile.test.package.tk-pg-greedy-synth]
150154
opt-level = 1
151155
[profile.test.package.tk-pg-rebase]

‎pg-libs/pg-passes/tk-pg-canonical-form/src/lib.rs‎

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Pass to convert a Pauli graph into canonical form.
22
use tk_pg_core::{
3-
BlackBoxData, ConditionalBoxData, GateData, GateType, Op, PGPass, PauliGraph, TableauData,
3+
BlackBoxData, ConditionalBoxData, GateData, GateType, Op, PGPass, Pauli, PauliGraph,
4+
RotationData, TableauData,
45
};
56
use tk_pg_ir_kernels::{PGTableau, get_dagger, is_clifford, is_clifford_gate_type};
67
use tk_pg_qm_tableau::Tableau as QubitMajorTableau;
@@ -260,9 +261,7 @@ fn process_op(
260261
if compose {
261262
tab.precompose_op(&op);
262263
} else {
263-
for conjugated_op in tab.conjugate(&op) {
264-
pg.add_op(conjugated_op);
265-
}
264+
process_op(pg, &op, tab, forward, cliff_eval);
266265
}
267266
}
268267
return;
@@ -283,7 +282,25 @@ fn process_op(
283282
if !forward {
284283
conjugated_ops.reverse();
285284
}
286-
for conjugated_op in conjugated_ops {
285+
for mut conjugated_op in conjugated_ops {
286+
if cliff_eval && let Op::Rotation { data } = &mut conjugated_op {
287+
let angle = data.get_angle();
288+
if angle.is_finite() && angle.abs() >= 0.5 {
289+
let remainder = angle % 0.5;
290+
let correction = angle - remainder;
291+
// The string is already conjugated, so append the correction to the tableau.
292+
// Forward traversal maintains the inverse tableau.
293+
if data.get_string().iter().any(|p| *p != Pauli::I) {
294+
tab.postcompose_op(&Op::Rotation {
295+
data: RotationData::new(
296+
data.get_string().clone(),
297+
if forward { -correction } else { correction },
298+
),
299+
});
300+
}
301+
*data = RotationData::new(data.get_string().clone(), remainder);
302+
}
303+
}
287304
pg.add_op(conjugated_op);
288305
}
289306
}
@@ -339,7 +356,9 @@ fn to_canonical_form(pg: &PauliGraph, forward: bool, cliff_eval: bool) -> PauliG
339356
/// tableaux, interleaved with any obstructions and the non-Clifford ops (rotations, measures,
340357
/// resets, conditional operations) that are conjugated through and left in place.
341358
/// `cliff_eval` controls whether rotations with a Clifford angle are folded into the tableau
342-
/// (`true`) or kept as explicit rotations (`false`).
359+
/// (`true`) or kept as explicit rotations (`false`). When enabled, it also extracts
360+
/// Clifford corrections from unconditional rotations, leaving their angles strictly
361+
/// between -0.5 and 0.5 half turns.
343362
///
344363
/// # Panics
345364
///
@@ -369,7 +388,9 @@ impl CanonicalFormPass {
369388
self.forward = forward;
370389
self
371390
}
372-
/// Set whether to evaluate Clifford angle rotations as Clifford gates or keep them as rotations.
391+
/// Set whether to absorb Clifford rotations and the Clifford part of larger angles
392+
/// into the tableau. Enabled by default; remaining unconditional rotation angles
393+
/// lie strictly between -0.5 and 0.5 half turns.
373394
pub fn with_cliff_eval(mut self, cliff_eval: bool) -> Self {
374395
self.cliff_eval = cliff_eval;
375396
self
@@ -455,6 +476,87 @@ mod tests {
455476
assert!(compare_unitaries_via_tk(&pg, &transformed));
456477
}
457478

479+
#[rstest]
480+
#[case(true, true, 0.75, 0.25)]
481+
#[case(false, true, 0.75, 0.25)]
482+
#[case(true, true, -0.75, -0.25)]
483+
#[case(false, true, -0.75, -0.25)]
484+
#[case(true, true, 2.1, 0.1)]
485+
#[case(false, true, 2.1, 0.1)]
486+
#[case(true, true, -2.1, -0.1)]
487+
#[case(false, true, -2.1, -0.1)]
488+
#[case(true, false, 0.75, 0.75)]
489+
#[case(false, false, 0.75, 0.75)]
490+
#[case(true, false, -0.75, -0.75)]
491+
#[case(false, false, -0.75, -0.75)]
492+
#[case(true, false, 2.1, 2.1)]
493+
#[case(false, false, 2.1, 2.1)]
494+
#[case(true, false, -2.1, -2.1)]
495+
#[case(false, false, -2.1, -2.1)]
496+
fn test_extract_clifford_corrections(
497+
#[case] forward: bool,
498+
#[case] cliff_eval: bool,
499+
#[case] angle: f64,
500+
#[case] expected_angle: f64,
501+
) {
502+
let mut pg = PauliGraph::new(2);
503+
pg.add_op(Op::Gate {
504+
data: GateData::new(GateType::RZ, vec![0]).with_params(vec![angle]),
505+
});
506+
pg.add_op(Op::Rotation {
507+
data: RotationData::new(vec![Pauli::I, Pauli::Z], angle),
508+
});
509+
let pass = CanonicalFormPass::new()
510+
.with_forward(forward)
511+
.with_cliff_eval(cliff_eval);
512+
let transformed = pass.transform(&pg);
513+
514+
assert_eq!(transformed.get_ops().len(), 3);
515+
for op in transformed.get_ops() {
516+
if let Op::Rotation { data } = op {
517+
assert!((data.get_angle() - expected_angle).abs() < 1e-10);
518+
}
519+
}
520+
assert!(compare_unitaries_via_tk(&pg, &transformed));
521+
}
522+
523+
#[rstest]
524+
#[case(true)]
525+
#[case(false)]
526+
fn test_clifford_corrections_propagate(#[case] forward: bool) {
527+
let mut pg = PauliGraph::new(2);
528+
pg.add_op(Op::Gate {
529+
data: GateData::new(GateType::H, vec![0]),
530+
});
531+
pg.add_op(Op::Gate {
532+
data: GateData::new(GateType::RX, vec![0]).with_params(vec![0.75]),
533+
});
534+
pg.add_op(Op::Gate {
535+
data: GateData::new(GateType::RY, vec![0]).with_params(vec![-0.75]),
536+
});
537+
pg.add_op(Op::Rotation {
538+
data: RotationData::new(vec![Pauli::X, Pauli::Y], 1.25),
539+
});
540+
pg.add_op(Op::Gate {
541+
data: GateData::new(GateType::PHASEDX, vec![0]).with_params(vec![0.75, 1.25]),
542+
});
543+
pg.add_op(Op::Gate {
544+
data: GateData::new(GateType::ZZPHASE, vec![0, 1]).with_params(vec![-1.25]),
545+
});
546+
pg.add_op(Op::Gate {
547+
data: GateData::new(GateType::H, vec![1]),
548+
});
549+
let pass = CanonicalFormPass::new().with_forward(forward);
550+
let transformed = pass.transform(&pg);
551+
552+
for op in transformed.get_ops() {
553+
if let Op::Rotation { data } = op {
554+
assert!(data.get_angle().abs() < 0.5);
555+
}
556+
}
557+
assert!(compare_unitaries_via_tk(&pg, &transformed));
558+
}
559+
458560
#[rstest]
459561
#[case(true, true, vec![0.5, 1.0], 1)]
460562
#[case(false, true, vec![0.5, 1.0], 1)]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "tk-pg-t-optimize"
3+
version = "0.1.0"
4+
description = "T-count optimization passes for Pauli graphs"
5+
edition = { workspace = true }
6+
rust-version = { workspace = true }
7+
license = { workspace = true }
8+
documentation = "https://docs.rs/tk-pg-t-optimize"
9+
homepage = { workspace = true }
10+
repository = { workspace = true }
11+
keywords = { workspace = true }
12+
categories = { workspace = true }
13+
14+
[features]
15+
default = []
16+
unstable_simd = []
17+
18+
[dependencies]
19+
bitvec = "1.0.1"
20+
tk-pg-core = { path = "../../tk-pg-core", version = "0.1.0" }
21+
tk-pg-canonical-form = { path = "../tk-pg-canonical-form", version = "0.1.0" }
22+
tk-pg-ir-kernels = { path = "../../pg-kernels/tk-pg-ir-kernels", version = "0.1.0" }
23+
tk-pg-qm-tableau = { path = "../../tk-pg-qm-tableau", version = "0.1.0" }
24+
25+
[dev-dependencies]
26+
tk-pg-converter = { path = "../../pg-converters/tk-pg-converter", version = "0.1.0" }
27+
28+
[lints]
29+
workspace = true
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# tk-pg-t-optimize
2+
3+
`tk-pg-t-optimize` provides `TOptimizationPass`, which reduces the T count of
4+
Clifford + T Pauli graphs using phase polynomial resynthesis by the TODD
5+
algorithm. Optional ancillas allow Hadamard gadgets to combine more rotations
6+
into each optimization batch, increasing the performance of the pass.
7+
8+
The pass expects a pauli graph which has been processed by the
9+
`CanonicalFormPass` pass, and performs best after also applying phase folding
10+
from the `RotationMergingPass`. Input measurements, resets, conditional operations,
11+
black boxes and arbitrary Rz gates are not currently supported.
12+
13+
By default, the pass uses no ancillas. `with_ancilla_budget` reserves the last
14+
qubits of the input graph as ancillas; these must already be present, idle, and
15+
prepared in zero. The pass does not allocate additional qubits.
16+
17+
Using ancillas hadamard gadgets which contain measurements, resets, and
18+
conditional Clifford.
19+
20+
To synthesize the result into gates apply`GreedySynthPass`.
21+
22+
Bit-vector operations are scalar by default. The optional `unstable_simd`
23+
feature enables portable SIMD and requires a nightly Rust toolchain.
24+
25+
## License
26+
27+
This project is licensed under Apache License, Version 2.0 ([LICENCE][] or <https://www.apache.org/licenses/LICENSE-2.0>).
28+
29+
[LICENCE]: https://github.com/quantinuum/tket2/blob/main/LICENCE

0 commit comments

Comments
 (0)