From c1658de578f65577b7908d6798356abd02774d8d Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 03:27:30 +0000 Subject: [PATCH 01/22] Absorb T gate S corrections into Pauli frame in unbounded-weight mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In unbounded-weight mode (no Clifford products in the circuit), T gate failures no longer inject a physical S gate. Instead, a per-qubit parity flag is toggled at failure time (O(weight)) and applied just-in-time when each subsequent product is first scheduled, patching its operators in-place (X↔Y per the S conjugation tableau; Z unchanged). Two failures on the same qubit automatically cancel via XOR (S²=I). The jit_corrected guard prevents double-application on T gate retries. Bounded-weight and --no-t-failures modes are unaffected; verified bit-identical output against baseline. --- src/circuit.rs | 4 + src/scheduler.rs | 241 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 206 insertions(+), 39 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index 88cba1c..7b01de2 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -95,6 +95,10 @@ impl Circuit { &self.pps[id as usize] } + pub(crate) fn product_mut(&mut self, id: i32) -> &mut PauliProduct { + &mut self.pps[id as usize] + } + pub(crate) fn n_products(&self) -> usize { self.pps.len() } diff --git a/src/scheduler.rs b/src/scheduler.rs index 7967f47..faaa6ca 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -209,6 +209,12 @@ pub(crate) struct Scheduler { current_lcycle: usize, /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). recovery_t_ids: Vec, + /// Per-qubit accumulated S correction parity; true = odd number of pending corrections. + /// Only used in unbounded-weight mode. + runtime_s_parity: Vec, + /// Per-product flag; true = operators have already been patched by JIT S correction. + /// Prevents double-application when a failed T gate is retried on its recovery lcycle. + jit_corrected: Vec, } impl Scheduler { @@ -230,6 +236,8 @@ impl Scheduler { let n_bus_qubits = topo.n_bus_qubits; let n_magic_qubits = topo.n_magic_qubits; let n_nodes = topo.n_nodes; + let n_circuit_qubits = circuit.n_qubits; + let n_circuit_products = circuit.n_products(); let mut timers = AccumTimers::new(); let loop_timer = timers.add_or_get("schedule loop"); let other_timer = timers.add_or_get("other "); @@ -264,9 +272,15 @@ impl Scheduler { pp_paths: Vec::new(), current_lcycle: 0, recovery_t_ids: Vec::new(), + runtime_s_parity: vec![false; n_circuit_qubits], + jit_corrected: vec![false; n_circuit_products], } } + fn is_unbounded_weight_mode(&self) -> bool { + !self.input.circuit.pps.iter().any(|pp| pp.gate_type.is_clifford()) + } + pub(crate) fn count_t_products(&self) -> usize { (0..self.input.circuit.n_products()) .filter(|&id| self.input.circuit.product(id as i32).gate_type.is_t()) @@ -554,47 +568,53 @@ impl Scheduler { self.precomputed_terminals = vec![Vec::new(); n_products]; self.precomputed_root_info = vec![Vec::new(); n_products]; for pp_id in 0..n_products { - let pp = self.input.circuit.product(pp_id as i32).clone(); - let terminals = operators_to_node_ids(&self.input.topo, &pp.operators); - let mut root_info: Vec<(bool, Vec, Vec)> = - Vec::with_capacity(terminals.len()); - for &term_id in &terminals { - let node = self.input.topo.node(term_id); - // is_paired: this terminal's paired data node is also a terminal - // (i.e. the operator is Y-basis, producing both X and Z data nodes). - let is_paired = - node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); - let mut preferred: Vec = Vec::new(); - let mut side: Vec = Vec::new(); - if is_paired { - // X nodes look downward (toward paired Z), Z nodes look upward. - let is_x = self.input.topo.label(term_id).contains('X'); - for &nb_id in node.nbs_slice() { - let nb = self.input.topo.node(nb_id); - if !nb.is_routing() { - continue; - } - if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { - preferred.push(nb_id); - } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { - side.push(nb_id); - } + self.recompute_terminals_for_product(pp_id); + } + println!("Precomputed terminals and root candidates for {} products", n_products); + } + + /// Recomputes terminal node IDs and root candidates for a single product. + /// Called once for all products at startup and again when a JIT S correction + /// changes a product's operator bases (X↔Y), altering its terminal set. + fn recompute_terminals_for_product(&mut self, pp_id: usize) { + let pp = self.input.circuit.product(pp_id as i32).clone(); + let terminals = operators_to_node_ids(&self.input.topo, &pp.operators); + let mut root_info: Vec<(bool, Vec, Vec)> = Vec::with_capacity(terminals.len()); + for &term_id in &terminals { + let node = self.input.topo.node(term_id); + // is_paired: this terminal's paired data node is also a terminal + // (i.e. the operator is Y-basis, producing both X and Z data nodes). + let is_paired = + node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); + let mut preferred: Vec = Vec::new(); + let mut side: Vec = Vec::new(); + if is_paired { + // X nodes look downward (toward paired Z), Z nodes look upward. + let is_x = self.input.topo.label(term_id).contains('X'); + for &nb_id in node.nbs_slice() { + let nb = self.input.topo.node(nb_id); + if !nb.is_routing() { + continue; } - } else { - // Unpaired terminal: only same-row side nbs are valid roots. - for &nb_id in node.nbs_slice() { - let nb = self.input.topo.node(nb_id); - if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { - preferred.push(nb_id); - } + if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { + preferred.push(nb_id); + } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + side.push(nb_id); + } + } + } else { + // Unpaired terminal: only same-row side nbs are valid roots. + for &nb_id in node.nbs_slice() { + let nb = self.input.topo.node(nb_id); + if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + preferred.push(nb_id); } } - root_info.push((is_paired, preferred, side)); } - self.precomputed_terminals[pp_id] = terminals; - self.precomputed_root_info[pp_id] = root_info; + root_info.push((is_paired, preferred, side)); } - println!("Precomputed terminals and root candidates for {} products", n_products); + self.precomputed_terminals[pp_id] = terminals; + self.precomputed_root_info[pp_id] = root_info; } fn mark_nodes_used(&mut self, node_ids: &[u16]) { @@ -703,6 +723,10 @@ impl Scheduler { // before the loop body calls `&mut self` methods. let ids_to_process: Vec = self.remaining_ids_buf.clone(); for &pp_id in &ids_to_process { + // Apply pending S corrections before consulting the precomputed tree. + // If any basis changed, the stale tree is evicted inside apply_jit_s_correction + // and the product will fall through to sched_remaining for fresh routing. + self.apply_jit_s_correction(pp_id); // Clone the Rc to end the immutable borrow on `precomputed_clifford_trees`. let Some(tree) = self.precomputed_clifford_trees.get(&pp_id).map(Rc::clone) else { continue; @@ -741,14 +765,62 @@ impl Scheduler { } } + /// Applies any pending S corrections to product `pp_id`'s operators in-place, + /// then recomputes routing caches if any basis changed (X↔Y). + /// + /// Only runs in unbounded-weight mode and only once per product (guarded by + /// `jit_corrected`) so retried T gates do not get double-corrected. + /// + /// If a basis change occurs, also evicts any stale precomputed Clifford tree for + /// the product — the tree was built for the old operator bases and is now wrong. + /// The product will then fall through to `sched_remaining()` for fresh routing. + fn apply_jit_s_correction(&mut self, pp_id: i32) { + if !self.is_unbounded_weight_mode() || self.jit_corrected[pp_id as usize] { + return; + } + self.jit_corrected[pp_id as usize] = true; + let mut needs_recompute = false; + let n_ops = self.input.circuit.product(pp_id).operators.len(); + for i in 0..n_ops { + let qubit = self.input.circuit.product(pp_id).operators[i].qubit; + if !self.runtime_s_parity[qubit as usize] { + continue; + } + let original = self.input.circuit.product(pp_id).operators[i].basis; + let new_basis = match original { + 'X' => 'Y', + 'Y' => 'X', + _ => original, // Z → Z unchanged + }; + if new_basis != original { + self.input.circuit.product_mut(pp_id).operators[i].basis = new_basis; + needs_recompute = true; + info_sched!( + " S correction JIT: product {} qubit {} {} → {}", + pp_id, qubit, original, new_basis + ); + } + } + if needs_recompute { + self.recompute_terminals_for_product(pp_id as usize); + // Any precomputed Clifford tree was built for the old operator bases + // and is now stale; drop it so the product re-routes via sched_remaining. + self.precomputed_clifford_trees.remove(&pp_id); + } + } + /// Second pass: greedily schedule T gates, measurements, and S/SX gates. /// T gates are skipped when no magic state is available. fn sched_remaining(&mut self, n_avail_magic: &mut usize, plotting: bool) { let _timer = accum_start!(self.timers); for i in 0..self.pps_pending.len() { let pp_id = self.pps_pending[i].id; + self.apply_jit_s_correction(pp_id); let pp = self.input.circuit.product(pp_id); - if Self::should_precompute(pp) { + // Skip if this product would normally use a precomputed Clifford tree AND + // the tree is still valid. If JIT correction evicted the tree, fall through + // to route it fresh via Steiner tree. + if Self::should_precompute(pp) && self.precomputed_clifford_trees.contains_key(&pp_id) { continue; } let (pp_id, gate_type) = (pp.id, pp.gate_type); @@ -882,9 +954,12 @@ impl Scheduler { plotting, ) } else { + // A should_precompute product can reach here if its precomputed Clifford tree + // was evicted by a JIT S correction; route it fresh via Steiner tree. debug_assert!( - !Self::should_precompute(self.input.circuit.product(pp_id)), - "should_precompute product {:?} reached Steiner path", + !Self::should_precompute(self.input.circuit.product(pp_id)) + || !self.precomputed_clifford_trees.contains_key(&pp_id), + "should_precompute product {:?} reached Steiner path with valid precomputed tree", pp_id ); match self.stree_computation.compute( @@ -927,6 +1002,16 @@ impl Scheduler { self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); self.recovery_t_ids = t_recovery_ids; + // Toggle per-qubit S correction parity for each failed T gate. + // Two failures on the same qubit cancel automatically (S² = Z, Z conjugation is identity). + if self.is_unbounded_weight_mode() { + for &pp_id in &t_failed_ids { + let ops: Vec<_> = self.input.circuit.product(pp_id).operators.clone(); + for op in ops { + self.runtime_s_parity[op.qubit as usize] ^= true; + } + } + } self.unlock_children(&t_failed_ids); self.advance_clifford_state(); debug_sched!( @@ -1518,4 +1603,82 @@ mod tests { sched.t_gate_failures ); } + + #[test] + fn unbounded_mode_detected_when_no_clifford_products() { + // A circuit with only T products has no Clifford products → unbounded mode. + let lines = &["+X___", "-_X__"]; + let sched = run_scheduler(lines, 0); + assert!(sched.is_unbounded_weight_mode()); + } + + #[test] + fn bounded_mode_detected_with_s_product() { + // A circuit with an S product is NOT in unbounded mode. + let lines = &["+X___", "+X___", "-_X__"]; + let sched = run_scheduler(lines, 0); + assert!(!sched.is_unbounded_weight_mode()); + } + + #[test] + fn scheduler_completes_with_jit_correction_enabled() { + // Two independent T gates on separate qubits; any failures should be + // handled by JIT S correction without panicking. + let lines = &["+X___", "-_X__"]; + let sched = run_scheduler(lines, 42); + assert!(!sched.lcycle_scheduled.is_empty()); + } + + #[test] + fn z_basis_operator_unchanged_after_t_failure() { + // Z → Z under S conjugation; an M product on the same qubit keeps Z. + // The scheduler must complete without panicking. + let lines = &["+X___", "+Z___"]; + let sched = run_scheduler(lines, 42); + assert!(!sched.lcycle_scheduled.is_empty()); + } + + #[test] + fn double_t_failure_cancels_parity() { + // Two T gates on the same qubit whose failures cancel (S² = Z = identity on routing). + // Use enough products and seeds so at least one seed produces two failures. + let lines = &["+X___", "+X___", "+X___"]; + for seed in 0u32..20 { + // Just verify the scheduler never panics regardless of how many failures occur. + let sched = run_scheduler(lines, seed); + assert!(!sched.lcycle_scheduled.is_empty()); + } + } + + #[test] + fn jit_correction_patches_operator_after_upstream_t_failure() { + // With seed 0, gates 0 and 1 both fail on a 3-gate all-X chain: + // gate 0 fails → parity[0] = true + // gate 1 scheduled → JIT fires: X → Y for product 1 + // gate 1 fails → parity[0] toggles back to false (S² = identity) + // gate 2 scheduled → parity[0] = false, no correction; stays X + let lines = &["+X___", "+X___", "+X___"]; + let sched = run_scheduler(lines, 0); + + // Both failures must have actually occurred for this test to be meaningful. + assert_eq!(sched.t_gate_failures, 2, "expected exactly 2 T failures with seed 0"); + + // apply_jit_s_correction() was called exactly once per product. + assert!(sched.jit_corrected[0], "product 0 should be marked jit_corrected"); + assert!(sched.jit_corrected[1], "product 1 should be marked jit_corrected"); + assert!(sched.jit_corrected[2], "product 2 should be marked jit_corrected"); + + // Product 1: gate 0 failed before it was scheduled → X patched to Y. + assert_eq!( + sched.input.circuit.product(1).operators[0].basis, 'Y', + "product 1 operator should be X→Y after gate 0 failure" + ); + + // Product 2: gate 0 and gate 1 both failed before it was scheduled. + // Two S corrections cancel (S² = I), so the operator stays X. + assert_eq!( + sched.input.circuit.product(2).operators[0].basis, 'X', + "product 2 operator should remain X: double failure cancels" + ); + } } From af4e88a13bab96c7b99db9097d397bc3c65f5c62 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 03:32:26 +0000 Subject: [PATCH 02/22] Add --record-cultivation-dist flag to control cultivation dist output The cultivation-time distribution file is now written only when -C / --record-cultivation-dist is passed; it is suppressed by default. --- src/puremagic.rs | 4 ++++ src/scheduler.rs | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/puremagic.rs b/src/puremagic.rs index 9675c6d..e20fa9b 100644 --- a/src/puremagic.rs +++ b/src/puremagic.rs @@ -67,6 +67,9 @@ struct Args { /// Disable T gate failures (every T gate succeeds on first attempt) #[arg(short = 'F', long)] no_t_failures: bool, + /// Record normalized cultivation-time distribution to .cultivation_dist + #[arg(short = 'C', long)] + record_cultivation_dist: bool, /// Number of ancilla between each data patch (all magic routing only) #[arg(short, long, default_value = "1")] ancilla_rows: usize, @@ -146,6 +149,7 @@ fn main() -> Result<(), Box> { args.plot.join(" "), args.rseed, args.no_t_failures, + args.record_cultivation_dist, ); let (tot_lcycles, n_scheduled) = sched.sched_circuit()?; diff --git a/src/scheduler.rs b/src/scheduler.rs index faaa6ca..1e4212b 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -189,6 +189,7 @@ pub(crate) struct Scheduler { pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, + record_cultivation_dist: bool, /// Reusable buffers to avoid per-lcycle allocations. terminals_buf: Vec, scheduled_ids_buf: Vec, @@ -220,7 +221,7 @@ pub(crate) struct Scheduler { impl Scheduler { pub(crate) fn new( circuit: Circuit, topo: TopoGraph, magic_state_lambda: f64, log_level: &str, - plot_option: String, rseed: u32, no_t_failures: bool, + plot_option: String, rseed: u32, no_t_failures: bool, record_cultivation_dist: bool, ) -> Self { if log_level != "none" { let trace_fname = format!("{}.sched_trace", circuit_stem(&circuit.circuit_fname)); @@ -257,6 +258,7 @@ impl Scheduler { stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, + record_cultivation_dist, terminals_buf: Vec::new(), scheduled_ids_buf: Vec::new(), children_buf: Vec::new(), @@ -1194,11 +1196,12 @@ impl Scheduler { println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); - // Write normalized cultivation-time distribution to file. - let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); - match self.write_cultivation_dist(&dist_fname, min, max) { - Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), - Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + if self.record_cultivation_dist { + let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); + match self.write_cultivation_dist(&dist_fname, min, max) { + Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), + Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + } } } @@ -1506,7 +1509,7 @@ mod tests { let mut topo = TopoGraph::new(); topo.set_topo(4, &"dummy".to_string(), &"".to_string(), &0, true, 1, false); let mut sched = - Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false); + Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false, false); sched.sched_circuit().expect("sched_circuit failed"); sched } From 90d8fb0db4134f4e5525e116f6f4dc21c5ea3b4f Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Thu, 23 Jul 2026 17:36:48 -1000 Subject: [PATCH 03/22] Added a wide range of QFT files --- data/all_compiled/qft_N008.cliffordt.qasm | 3 +++ data/all_compiled/qft_N008.qasm | 3 +++ data/all_compiled/qft_N024.cliffordt.qasm | 3 +++ data/all_compiled/qft_N024.qasm | 3 +++ data/all_compiled/qft_N032.cliffordt.qasm | 3 +++ data/all_compiled/qft_N032.qasm | 3 +++ data/all_compiled/qft_N040.cliffordt.qasm | 3 +++ data/all_compiled/qft_N040.qasm | 3 +++ data/all_compiled/qft_N048.cliffordt.qasm | 3 +++ data/all_compiled/qft_N048.qasm | 3 +++ data/all_compiled/qft_N056.cliffordt.qasm | 3 +++ data/all_compiled/qft_N056.qasm | 3 +++ data/all_compiled/qft_N064.cliffordt.qasm | 3 +++ data/all_compiled/qft_N064.qasm | 3 +++ data/all_compiled/qft_N072.cliffordt.qasm | 3 +++ data/all_compiled/qft_N072.qasm | 3 +++ data/all_compiled/qft_N080.cliffordt.qasm | 3 +++ data/all_compiled/qft_N080.qasm | 3 +++ data/all_compiled/qft_N088.cliffordt.qasm | 3 +++ data/all_compiled/qft_N088.qasm | 3 +++ data/all_compiled/qft_N096.cliffordt.qasm | 3 +++ data/all_compiled/qft_N096.qasm | 3 +++ data/transpiled/max-weight-0/qft_N008.trans | 3 +++ data/transpiled/max-weight-0/qft_N024.trans | 3 +++ data/transpiled/max-weight-0/qft_N032.trans | 3 +++ data/transpiled/max-weight-0/qft_N040.trans | 3 +++ data/transpiled/max-weight-0/qft_N048.trans | 3 +++ data/transpiled/max-weight-0/qft_N056.trans | 3 +++ data/transpiled/max-weight-0/qft_N064.trans | 3 +++ data/transpiled/max-weight-0/qft_N072.trans | 3 +++ data/transpiled/max-weight-0/qft_N080.trans | 3 +++ data/transpiled/max-weight-0/qft_N088.trans | 3 +++ data/transpiled/max-weight-0/qft_N096.trans | 3 +++ data/transpiled/max-weight-1/qft_N008.trans | 3 +++ data/transpiled/max-weight-1/qft_N024.trans | 3 +++ data/transpiled/max-weight-1/qft_N032.trans | 3 +++ data/transpiled/max-weight-1/qft_N040.trans | 3 +++ data/transpiled/max-weight-1/qft_N048.trans | 3 +++ data/transpiled/max-weight-1/qft_N056.trans | 3 +++ data/transpiled/max-weight-1/qft_N064.trans | 3 +++ data/transpiled/max-weight-1/qft_N072.trans | 3 +++ data/transpiled/max-weight-1/qft_N080.trans | 3 +++ data/transpiled/max-weight-1/qft_N088.trans | 3 +++ data/transpiled/max-weight-1/qft_N096.trans | 3 +++ 44 files changed, 132 insertions(+) create mode 100644 data/all_compiled/qft_N008.cliffordt.qasm create mode 100644 data/all_compiled/qft_N008.qasm create mode 100644 data/all_compiled/qft_N024.cliffordt.qasm create mode 100644 data/all_compiled/qft_N024.qasm create mode 100644 data/all_compiled/qft_N032.cliffordt.qasm create mode 100644 data/all_compiled/qft_N032.qasm create mode 100644 data/all_compiled/qft_N040.cliffordt.qasm create mode 100644 data/all_compiled/qft_N040.qasm create mode 100644 data/all_compiled/qft_N048.cliffordt.qasm create mode 100644 data/all_compiled/qft_N048.qasm create mode 100644 data/all_compiled/qft_N056.cliffordt.qasm create mode 100644 data/all_compiled/qft_N056.qasm create mode 100644 data/all_compiled/qft_N064.cliffordt.qasm create mode 100644 data/all_compiled/qft_N064.qasm create mode 100644 data/all_compiled/qft_N072.cliffordt.qasm create mode 100644 data/all_compiled/qft_N072.qasm create mode 100644 data/all_compiled/qft_N080.cliffordt.qasm create mode 100644 data/all_compiled/qft_N080.qasm create mode 100644 data/all_compiled/qft_N088.cliffordt.qasm create mode 100644 data/all_compiled/qft_N088.qasm create mode 100644 data/all_compiled/qft_N096.cliffordt.qasm create mode 100644 data/all_compiled/qft_N096.qasm create mode 100644 data/transpiled/max-weight-0/qft_N008.trans create mode 100644 data/transpiled/max-weight-0/qft_N024.trans create mode 100644 data/transpiled/max-weight-0/qft_N032.trans create mode 100644 data/transpiled/max-weight-0/qft_N040.trans create mode 100644 data/transpiled/max-weight-0/qft_N048.trans create mode 100644 data/transpiled/max-weight-0/qft_N056.trans create mode 100644 data/transpiled/max-weight-0/qft_N064.trans create mode 100644 data/transpiled/max-weight-0/qft_N072.trans create mode 100644 data/transpiled/max-weight-0/qft_N080.trans create mode 100644 data/transpiled/max-weight-0/qft_N088.trans create mode 100644 data/transpiled/max-weight-0/qft_N096.trans create mode 100644 data/transpiled/max-weight-1/qft_N008.trans create mode 100644 data/transpiled/max-weight-1/qft_N024.trans create mode 100644 data/transpiled/max-weight-1/qft_N032.trans create mode 100644 data/transpiled/max-weight-1/qft_N040.trans create mode 100644 data/transpiled/max-weight-1/qft_N048.trans create mode 100644 data/transpiled/max-weight-1/qft_N056.trans create mode 100644 data/transpiled/max-weight-1/qft_N064.trans create mode 100644 data/transpiled/max-weight-1/qft_N072.trans create mode 100644 data/transpiled/max-weight-1/qft_N080.trans create mode 100644 data/transpiled/max-weight-1/qft_N088.trans create mode 100644 data/transpiled/max-weight-1/qft_N096.trans diff --git a/data/all_compiled/qft_N008.cliffordt.qasm b/data/all_compiled/qft_N008.cliffordt.qasm new file mode 100644 index 0000000..bb7ff7c --- /dev/null +++ b/data/all_compiled/qft_N008.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0aabbd7574697c86af5fe4a0becf314ccc65e226477b1c8bea89dbd7c04de157 +size 289605 diff --git a/data/all_compiled/qft_N008.qasm b/data/all_compiled/qft_N008.qasm new file mode 100644 index 0000000..b5d0408 --- /dev/null +++ b/data/all_compiled/qft_N008.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95a7e8adacf8a82fd9504d8a95bb36cb66c850a603a067ab4681ddba24fabcf1 +size 2564 diff --git a/data/all_compiled/qft_N024.cliffordt.qasm b/data/all_compiled/qft_N024.cliffordt.qasm new file mode 100644 index 0000000..c560345 --- /dev/null +++ b/data/all_compiled/qft_N024.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0fc16c27b3615a66d7a0f1aefa72152b1a94eacf291469145c3767d61f79146 +size 2669736 diff --git a/data/all_compiled/qft_N024.qasm b/data/all_compiled/qft_N024.qasm new file mode 100644 index 0000000..90e0dc9 --- /dev/null +++ b/data/all_compiled/qft_N024.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12cadd769e9f7659971ae6c3cdb59a0b83b16ba4a14f107b7ade3db1b822c3f3 +size 24694 diff --git a/data/all_compiled/qft_N032.cliffordt.qasm b/data/all_compiled/qft_N032.cliffordt.qasm new file mode 100644 index 0000000..96950b0 --- /dev/null +++ b/data/all_compiled/qft_N032.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e175d1ab48ccf541f010f4359766437dd4ca6ca5dcd430340fbe6e92ed1da4e2 +size 3984573 diff --git a/data/all_compiled/qft_N032.qasm b/data/all_compiled/qft_N032.qasm new file mode 100644 index 0000000..26a98f6 --- /dev/null +++ b/data/all_compiled/qft_N032.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d16ee71c325dbac6070d3f9783c92e68ef8e1bec1e8c8fec277dea45b5f1fe0 +size 45359 diff --git a/data/all_compiled/qft_N040.cliffordt.qasm b/data/all_compiled/qft_N040.cliffordt.qasm new file mode 100644 index 0000000..e805f82 --- /dev/null +++ b/data/all_compiled/qft_N040.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124de72dca2198140fd52dd27daf1656d9ebd93464ebf268882147c51457783e +size 5321394 diff --git a/data/all_compiled/qft_N040.qasm b/data/all_compiled/qft_N040.qasm new file mode 100644 index 0000000..c052dcf --- /dev/null +++ b/data/all_compiled/qft_N040.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e57d6eafc11d4d7b2d59f2a7cb6d872d8d7266ce25d9ceeedbea78079a30aa72 +size 73020 diff --git a/data/all_compiled/qft_N048.cliffordt.qasm b/data/all_compiled/qft_N048.cliffordt.qasm new file mode 100644 index 0000000..a3154de --- /dev/null +++ b/data/all_compiled/qft_N048.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf3f6587c4d891911fe3292a8a2f73cfc5e9f9b687bcb43ab78aec1d2a3886dc +size 6714507 diff --git a/data/all_compiled/qft_N048.qasm b/data/all_compiled/qft_N048.qasm new file mode 100644 index 0000000..d0f2347 --- /dev/null +++ b/data/all_compiled/qft_N048.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2960243961ae72383e6e4e8366e80c61821e16be7a47b31e47b9287460c8bba +size 106800 diff --git a/data/all_compiled/qft_N056.cliffordt.qasm b/data/all_compiled/qft_N056.cliffordt.qasm new file mode 100644 index 0000000..d454d76 --- /dev/null +++ b/data/all_compiled/qft_N056.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a6358a013b60987b55de3c9ce8cf4c30c890bfaffd17905b3cf803815737747 +size 7996316 diff --git a/data/all_compiled/qft_N056.qasm b/data/all_compiled/qft_N056.qasm new file mode 100644 index 0000000..11dab26 --- /dev/null +++ b/data/all_compiled/qft_N056.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a325c84160226f678e68d2fa00f6fabfdaf64e1e37cddbb1afc84c685b75361e +size 145124 diff --git a/data/all_compiled/qft_N064.cliffordt.qasm b/data/all_compiled/qft_N064.cliffordt.qasm new file mode 100644 index 0000000..c9441b6 --- /dev/null +++ b/data/all_compiled/qft_N064.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fa0f385c257fb659ffcd5520ec4c142c0e36551608093505f4f4c960cf24e58 +size 9366271 diff --git a/data/all_compiled/qft_N064.qasm b/data/all_compiled/qft_N064.qasm new file mode 100644 index 0000000..1fcf396 --- /dev/null +++ b/data/all_compiled/qft_N064.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c3a7dc93ab7cb89d800025fe087c2379d8b8334fa96ba796566505c255d38cf +size 187992 diff --git a/data/all_compiled/qft_N072.cliffordt.qasm b/data/all_compiled/qft_N072.cliffordt.qasm new file mode 100644 index 0000000..63d6969 --- /dev/null +++ b/data/all_compiled/qft_N072.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce9186c2cd34c87e10c3934485a9609fffb6ea4e16fff93c9ef1ba06b67db5ab +size 10677787 diff --git a/data/all_compiled/qft_N072.qasm b/data/all_compiled/qft_N072.qasm new file mode 100644 index 0000000..fe181f6 --- /dev/null +++ b/data/all_compiled/qft_N072.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81c75c276278da2e814d59654e9d963d3696c12929fc6d3aa238f471a87cf035 +size 235404 diff --git a/data/all_compiled/qft_N080.cliffordt.qasm b/data/all_compiled/qft_N080.cliffordt.qasm new file mode 100644 index 0000000..d193b07 --- /dev/null +++ b/data/all_compiled/qft_N080.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d485c5be1a5839ec1cd81b81691858ab7d1bbd169f228827853038360a579267 +size 11989783 diff --git a/data/all_compiled/qft_N080.qasm b/data/all_compiled/qft_N080.qasm new file mode 100644 index 0000000..e7ec268 --- /dev/null +++ b/data/all_compiled/qft_N080.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da0e2ea808f65d6643066c6abbb395c11824a6464c29f37056edcfc1db75b993 +size 287360 diff --git a/data/all_compiled/qft_N088.cliffordt.qasm b/data/all_compiled/qft_N088.cliffordt.qasm new file mode 100644 index 0000000..ab33bfd --- /dev/null +++ b/data/all_compiled/qft_N088.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3def7da1f2db90f1352363cbb62275d1a3cfe4430ccbc3d094e14b169ada81d4 +size 13362315 diff --git a/data/all_compiled/qft_N088.qasm b/data/all_compiled/qft_N088.qasm new file mode 100644 index 0000000..0438a4a --- /dev/null +++ b/data/all_compiled/qft_N088.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:139576376b16ef87a229f501bcba0d90e66715414139ac5a89cadbde0c07dafe +size 343860 diff --git a/data/all_compiled/qft_N096.cliffordt.qasm b/data/all_compiled/qft_N096.cliffordt.qasm new file mode 100644 index 0000000..5abf696 --- /dev/null +++ b/data/all_compiled/qft_N096.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbc2176d478e863aa4d52ee82289b9b501f0919c8b87fae41d21a83ad48daea5 +size 14739112 diff --git a/data/all_compiled/qft_N096.qasm b/data/all_compiled/qft_N096.qasm new file mode 100644 index 0000000..55dcafa --- /dev/null +++ b/data/all_compiled/qft_N096.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7df0634cfab702eb0299742c3acac959dee983fa598e5dc99dcb18318008fdf7 +size 404904 diff --git a/data/transpiled/max-weight-0/qft_N008.trans b/data/transpiled/max-weight-0/qft_N008.trans new file mode 100644 index 0000000..a4cb784 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N008.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dba8ecdee03eb33b43a02c4d9e56a4f69c290097d9e7c00e4d6d1acd3f99c6c +size 183339 diff --git a/data/transpiled/max-weight-0/qft_N024.trans b/data/transpiled/max-weight-0/qft_N024.trans new file mode 100644 index 0000000..45d0468 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N024.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b953b930851ca9ec893a780caead2b52d8af58e81dfa0ae958e914227c7cc0de +size 3529851 diff --git a/data/transpiled/max-weight-0/qft_N032.trans b/data/transpiled/max-weight-0/qft_N032.trans new file mode 100644 index 0000000..a1011f3 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N032.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b5a6ba790106bc13388c6eb1200611cb5731a8cb380378437c6d9455b992595 +size 6601318 diff --git a/data/transpiled/max-weight-0/qft_N040.trans b/data/transpiled/max-weight-0/qft_N040.trans new file mode 100644 index 0000000..82fc162 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N040.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2612dcd4f86d24761a18d193d0644a48a22373140e5d1d437a17b7383d6fa31b +size 10637415 diff --git a/data/transpiled/max-weight-0/qft_N048.trans b/data/transpiled/max-weight-0/qft_N048.trans new file mode 100644 index 0000000..7a242fc --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N048.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d17a995a2ba9f51a06c8af4d6abaa68db9ae668cffbd4cf62ffb53f186322413 +size 15718157 diff --git a/data/transpiled/max-weight-0/qft_N056.trans b/data/transpiled/max-weight-0/qft_N056.trans new file mode 100644 index 0000000..d71ef67 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N056.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9713c16c49a9f0744c0fee8eb611416a48f15b46e4c80bf47fb8a203d9bbddd +size 21436925 diff --git a/data/transpiled/max-weight-0/qft_N064.trans b/data/transpiled/max-weight-0/qft_N064.trans new file mode 100644 index 0000000..4c88c7d --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N064.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce03d15b3dde2a8b9c47aba9117518e1f1e3d4e8146d36a1d488b1c5a22e2e5f +size 28321326 diff --git a/data/transpiled/max-weight-0/qft_N072.trans b/data/transpiled/max-weight-0/qft_N072.trans new file mode 100644 index 0000000..4f7f725 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N072.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdc41173bec438bba1457d77c2bf9d3fb0f21cf98e9a62f2d2bbac7878e732ef +size 35928508 diff --git a/data/transpiled/max-weight-0/qft_N080.trans b/data/transpiled/max-weight-0/qft_N080.trans new file mode 100644 index 0000000..7ff5133 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N080.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef669cb40e5a82c36b9b11989c173e2599042298647d2f7a09364e8cbdb9d04 +size 44431795 diff --git a/data/transpiled/max-weight-0/qft_N088.trans b/data/transpiled/max-weight-0/qft_N088.trans new file mode 100644 index 0000000..bd8b0c7 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N088.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4cb20d949af31418e7953628640b7a42c31846f25589f5c739ba4b16d2688ae +size 54086382 diff --git a/data/transpiled/max-weight-0/qft_N096.trans b/data/transpiled/max-weight-0/qft_N096.trans new file mode 100644 index 0000000..6dd8e64 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N096.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61f87dd21a24abdd3404fffd84482ce8b635234d78387509e4240ee75cd1c4f8 +size 64688783 diff --git a/data/transpiled/max-weight-1/qft_N008.trans b/data/transpiled/max-weight-1/qft_N008.trans new file mode 100644 index 0000000..6d66742 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N008.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7450bc69c00a9f5760660ef36da07585c10f2757b8dede0a3911c4ab96747610 +size 189300 diff --git a/data/transpiled/max-weight-1/qft_N024.trans b/data/transpiled/max-weight-1/qft_N024.trans new file mode 100644 index 0000000..dbebb04 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N024.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad118ed8ed1fef8eb39b7c0dab3c0b838e1a4332ce2a7e7fe7017b413cc52e0e +size 3702451 diff --git a/data/transpiled/max-weight-1/qft_N032.trans b/data/transpiled/max-weight-1/qft_N032.trans new file mode 100644 index 0000000..79d4bb4 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N032.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05f438ec2621ec995c1d727d82bbfad95c126d04c74e50ea843ab25a23ce1733 +size 6937937 diff --git a/data/transpiled/max-weight-1/qft_N040.trans b/data/transpiled/max-weight-1/qft_N040.trans new file mode 100644 index 0000000..a5efd6c --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N040.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a728df129220825a004ef98cdebe08c839697d35446707b35ec567d5486599 +size 11189536 diff --git a/data/transpiled/max-weight-1/qft_N048.trans b/data/transpiled/max-weight-1/qft_N048.trans new file mode 100644 index 0000000..842afab --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N048.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c47afbd912e46a2157b135f16294c0359b74c376718f41a2977ae226acc9e2be +size 16580130 diff --git a/data/transpiled/max-weight-1/qft_N056.trans b/data/transpiled/max-weight-1/qft_N056.trans new file mode 100644 index 0000000..c64fd0c --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N056.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ea45bcb4bbc8bc14c0ba10c526362881a7981d6998af9bc18e143bc1285af5 +size 22622149 diff --git a/data/transpiled/max-weight-1/qft_N064.trans b/data/transpiled/max-weight-1/qft_N064.trans new file mode 100644 index 0000000..dcb2416 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N064.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2a87053110609284e42d4f649f440385c0c576c82831d28f3d4b1c48653dd92 +size 29920391 diff --git a/data/transpiled/max-weight-1/qft_N072.trans b/data/transpiled/max-weight-1/qft_N072.trans new file mode 100644 index 0000000..e050edd --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N072.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34c6fe7fdd9ade3c1fcbda064c793203392c2b6f149f422a73ff17380788fc55 +size 38014731 diff --git a/data/transpiled/max-weight-1/qft_N080.trans b/data/transpiled/max-weight-1/qft_N080.trans new file mode 100644 index 0000000..13dcf38 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N080.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b696554dda8ade431d36b0f921cee0ba4a2c62b1454cf39d20e7d03aba36a3f8 +size 47101096 diff --git a/data/transpiled/max-weight-1/qft_N088.trans b/data/transpiled/max-weight-1/qft_N088.trans new file mode 100644 index 0000000..696e3f3 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N088.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc267c16c22ec32fb6e714ee26e0254f8d0672ff938790d50688b6fbf77d0251 +size 57371652 diff --git a/data/transpiled/max-weight-1/qft_N096.trans b/data/transpiled/max-weight-1/qft_N096.trans new file mode 100644 index 0000000..1e17aa2 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N096.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45bdd8cba3859f1c20835cc2e2d4a8efab61897b7174e167697fd7f671adf143 +size 68669290 From 55818b02def450bd507c21706cc076cea907c7d4 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Thu, 23 Jul 2026 17:44:35 -1000 Subject: [PATCH 04/22] Updated gitignore --- .gitignore | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.gitignore b/.gitignore index aec029e..d5292d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,22 @@ __pycache__ results target notes +data/*.old +data/transpiled/python-version/ +data/transpiled_truncated/ +wisq +related +tableau +data/qasmbench/ +data/native_benchpress/ +data/pm_paper_circuits/ +data/lssp-data-silva-et-al/ +data/custom_qft_benchmarks/ +data/all_compiled_truncated/ +data/all_wisq/ +data/benchpress/ +data/clifford_rz/ +data/transpiled/max-weight-vary/ +data_processing/*out +data_processing/out_dir +data/*out \ No newline at end of file From 8d16cd56f3a629f9a416c67435dbecf3c222ec08 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 04:48:58 +0000 Subject: [PATCH 05/22] Cache unbounded-weight mode detection to avoid O(n_products) scan per product is_unbounded_weight_mode() was iterating all circuit products on every call, which was invoked for every product on every lcycle via apply_jit_s_correction. Now computed once at construction and stored as a bool field. --- src/scheduler.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 1e4212b..5665e3f 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -210,6 +210,9 @@ pub(crate) struct Scheduler { current_lcycle: usize, /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). recovery_t_ids: Vec, + /// True when the circuit has no Clifford products (all Cliffords absorbed into Pauli frame). + /// Computed once at construction; controls whether JIT S correction is active. + unbounded_weight_mode: bool, /// Per-qubit accumulated S correction parity; true = odd number of pending corrections. /// Only used in unbounded-weight mode. runtime_s_parity: Vec, @@ -239,6 +242,7 @@ impl Scheduler { let n_nodes = topo.n_nodes; let n_circuit_qubits = circuit.n_qubits; let n_circuit_products = circuit.n_products(); + let unbounded_weight_mode = !circuit.pps.iter().any(|pp| pp.gate_type.is_clifford()); let mut timers = AccumTimers::new(); let loop_timer = timers.add_or_get("schedule loop"); let other_timer = timers.add_or_get("other "); @@ -274,13 +278,14 @@ impl Scheduler { pp_paths: Vec::new(), current_lcycle: 0, recovery_t_ids: Vec::new(), + unbounded_weight_mode, runtime_s_parity: vec![false; n_circuit_qubits], jit_corrected: vec![false; n_circuit_products], } } fn is_unbounded_weight_mode(&self) -> bool { - !self.input.circuit.pps.iter().any(|pp| pp.gate_type.is_clifford()) + self.unbounded_weight_mode } pub(crate) fn count_t_products(&self) -> usize { From f643fc3fe5e5f724a3fb8acf61acc9af8727e7e1 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 05:02:12 +0000 Subject: [PATCH 06/22] Fix scheduled product count in unbounded-weight mode In bounded-weight mode each T gate failure costs an extra physical S correction product, so t_gate_failures was added to the reported count. In unbounded-weight mode failures are absorbed into the Pauli frame with no extra physical products, so the count should equal the circuit product count regardless of how many failures occur. --- src/scheduler.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 5665e3f..bb4f43d 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -430,9 +430,11 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // Return total lcycles and total "attempts" (successes + failures), so the - // caller can compute the true T-gate count including failed first attempts. - Ok((self.current_lcycle, self.scheduled_products.len() + self.t_gate_failures)) + // In bounded-weight mode, each T failure costs an extra physical S correction + // product, so we add t_gate_failures to the count. In unbounded-weight mode + // failures are absorbed into the Pauli frame with no extra physical products. + let extra = if self.unbounded_weight_mode { 0 } else { self.t_gate_failures }; + Ok((self.current_lcycle, self.scheduled_products.len() + extra)) } fn init_magic_nodes(&mut self) { From 55593db53606143905c70302e60abaacb9745fe6 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 07:14:00 +0000 Subject: [PATCH 07/22] Extend JIT S correction to bounded-weight mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toggle runtime_s_parity on T gate failure in bounded mode so that subsequent non-Clifford products receive the same X↔Y JIT correction already applied in unbounded mode. Clifford products are skipped in apply_jit_s_correction (parity propagates past them; the carry-forward routing handles their S correction physically). Remove the extra t_gate_failures term from the scheduled-product count — S corrections are now absorbed into the Pauli frame in both modes. --- src/scheduler.rs | 135 +++++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index bb4f43d..af87f4b 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -217,7 +217,6 @@ pub(crate) struct Scheduler { /// Only used in unbounded-weight mode. runtime_s_parity: Vec, /// Per-product flag; true = operators have already been patched by JIT S correction. - /// Prevents double-application when a failed T gate is retried on its recovery lcycle. jit_corrected: Vec, } @@ -284,9 +283,6 @@ impl Scheduler { } } - fn is_unbounded_weight_mode(&self) -> bool { - self.unbounded_weight_mode - } pub(crate) fn count_t_products(&self) -> usize { (0..self.input.circuit.n_products()) @@ -430,11 +426,9 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // In bounded-weight mode, each T failure costs an extra physical S correction - // product, so we add t_gate_failures to the count. In unbounded-weight mode - // failures are absorbed into the Pauli frame with no extra physical products. - let extra = if self.unbounded_weight_mode { 0 } else { self.t_gate_failures }; - Ok((self.current_lcycle, self.scheduled_products.len() + extra)) + // S corrections are absorbed into the Pauli frame via JIT correction in both + // bounded and unbounded weight modes; no extra physical S products are scheduled. + Ok((self.current_lcycle, self.scheduled_products.len())) } fn init_magic_nodes(&mut self) { @@ -777,14 +771,20 @@ impl Scheduler { /// Applies any pending S corrections to product `pp_id`'s operators in-place, /// then recomputes routing caches if any basis changed (X↔Y). /// - /// Only runs in unbounded-weight mode and only once per product (guarded by - /// `jit_corrected`) so retried T gates do not get double-corrected. + /// Only runs once per product (guarded by `jit_corrected`) so retried T gates + /// do not get double-corrected. Clifford products are skipped — their S + /// correction is handled by the bounded-mode carry-forward routing — but the + /// parity flag is left set so it propagates to the next non-Clifford product. /// /// If a basis change occurs, also evicts any stale precomputed Clifford tree for /// the product — the tree was built for the old operator bases and is now wrong. /// The product will then fall through to `sched_remaining()` for fresh routing. fn apply_jit_s_correction(&mut self, pp_id: i32) { - if !self.is_unbounded_weight_mode() || self.jit_corrected[pp_id as usize] { + if self.jit_corrected[pp_id as usize] { + return; + } + // Clifford products: parity carries forward past them. + if self.input.circuit.product(pp_id).gate_type.is_clifford() { return; } self.jit_corrected[pp_id as usize] = true; @@ -997,7 +997,7 @@ impl Scheduler { self.scheduled_ids_buf.extend(self.pp_paths.iter().map(|(id, _)| *id)); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); - // Only count T gates that are newly scheduled (not recovery lcycles) to + // Only count T gates that are newly scheduled (not bounded-mode recovery lcycles) to // keep the pool-size estimate accurate. let t_newly_scheduled = self .pp_paths @@ -1011,16 +1011,6 @@ impl Scheduler { self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); self.recovery_t_ids = t_recovery_ids; - // Toggle per-qubit S correction parity for each failed T gate. - // Two failures on the same qubit cancel automatically (S² = Z, Z conjugation is identity). - if self.is_unbounded_weight_mode() { - for &pp_id in &t_failed_ids { - let ops: Vec<_> = self.input.circuit.product(pp_id).operators.clone(); - for op in ops { - self.runtime_s_parity[op.qubit as usize] ^= true; - } - } - } self.unlock_children(&t_failed_ids); self.advance_clifford_state(); debug_sched!( @@ -1051,10 +1041,13 @@ impl Scheduler { /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids). /// - /// First-attempt T gates succeed with 50% probability (or always if `no_t_failures`). - /// Recovery-lcycle T gates (already in `failed_t_paths`) always succeed. - /// Failed gates are stored in `failed_t_paths` with the magic root trimmed off - /// so the routing subtree can be reused in the recovery lcycle. + /// **Unbounded-weight mode**: a "failed" T gate (50% coin) is treated as complete in this + /// lcycle — the S correction is absorbed into `runtime_s_parity` and children are unlocked + /// normally. No retry, no extra magic state consumed. Only the parity flag changes. + /// + /// **Bounded-weight mode**: parity is also toggled (JIT correction for non-Clifford + /// descendants), and the failed gate is stored in `failed_t_paths` with the magic root + /// trimmed off so the routing subtree can be carried forward to a recovery lcycle. fn process_t_gate_outcomes(&mut self) -> (Vec, Vec) { let mut t_failed_ids: Vec = Vec::new(); let mut t_recovery_ids: Vec = Vec::new(); @@ -1069,40 +1062,66 @@ impl Scheduler { } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { info_sched!(" T gate {} succeeded on first attempt", pp_id); } else { - t_failed_ids.push(pp_id); self.t_gate_failures += 1; - info_sched!( - " T gate {} failed (50% probability), recovery lcycle next", - pp_id - ); + if self.unbounded_weight_mode { + // S correction absorbed into Pauli frame: toggle parity and complete. + // No retry — the T gate finishes this lcycle like a success. + info_sched!( + " T gate {} S-corrected (parity toggled, no retry)", + pp_id + ); + let ops: Vec<_> = pp.operators.clone(); + for op in ops { + self.runtime_s_parity[op.qubit as usize] ^= true; + } + } else { + t_failed_ids.push(pp_id); + info_sched!( + " T gate {} failed (50% probability), recovery lcycle next", + pp_id + ); + // Toggle parity so JIT correction applies to subsequent + // non-Clifford products; carry-forward routing handles Clifford products. + let ops: Vec<_> = pp.operators.clone(); + for op in ops { + self.runtime_s_parity[op.qubit as usize] ^= true; + } + } } } } - let pp_paths_snapshot: Vec<(i32, Option>)> = - self.pp_paths.iter().map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))).collect(); - for (pp_id, opt_pp_path) in &pp_paths_snapshot { - let pp_id = *pp_id; - let pp = self.input.circuit.product(pp_id); - if !pp.gate_type.is_t() { - continue; - } - if t_failed_ids.contains(&pp_id) { - // Store the routing subtree without the magic root so the recovery - // lcycle can reuse the same data/routing nodes while finding a new - // magic state. When not plotting, fall back to just the terminal IDs. - let trimmed_opt_tree: Option> = opt_pp_path.as_ref().map(|tree| { - let mut t = (**tree).clone(); - t.trim_magic_root(); - Rc::new(t) - }); - let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { - trimmed.iter_nodes().collect() + // Bounded mode only: update failed_t_paths for retry routing. + if !self.unbounded_weight_mode { + let pp_paths_snapshot: Vec<(i32, Option>)> = self + .pp_paths + .iter() + .map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))) + .collect(); + for (pp_id, opt_pp_path) in &pp_paths_snapshot { + let pp_id = *pp_id; + let pp = self.input.circuit.product(pp_id); + if !pp.gate_type.is_t() { + continue; + } + if t_failed_ids.contains(&pp_id) { + // Store the routing subtree without the magic root so the recovery + // lcycle can reuse the same data/routing nodes while finding a new + // magic state. When not plotting, fall back to just the terminal IDs. + let trimmed_opt_tree: Option> = + opt_pp_path.as_ref().map(|tree| { + let mut t = (**tree).clone(); + t.trim_magic_root(); + Rc::new(t) + }); + let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { + trimmed.iter_nodes().collect() + } else { + self.precomputed_terminals[pp_id as usize].clone() + }; + self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); } else { - self.precomputed_terminals[pp_id as usize].clone() - }; - self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); - } else { - self.failed_t_paths.swap_remove(&pp_id); + self.failed_t_paths.swap_remove(&pp_id); + } } } (t_failed_ids, t_recovery_ids) @@ -1619,7 +1638,7 @@ mod tests { // A circuit with only T products has no Clifford products → unbounded mode. let lines = &["+X___", "-_X__"]; let sched = run_scheduler(lines, 0); - assert!(sched.is_unbounded_weight_mode()); + assert!(sched.unbounded_weight_mode); } #[test] @@ -1627,7 +1646,7 @@ mod tests { // A circuit with an S product is NOT in unbounded mode. let lines = &["+X___", "+X___", "-_X__"]; let sched = run_scheduler(lines, 0); - assert!(!sched.is_unbounded_weight_mode()); + assert!(!sched.unbounded_weight_mode); } #[test] From 435567c08a144f8e6211fd90109a4ea3e876697a Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 07:44:09 +0000 Subject: [PATCH 08/22] Add deterministic product-dropping optimization When a JIT S correction transforms a single-qubit product's operator to match the last completed single-qubit measurement on the same qubit, the product outcome is already known and it can be skipped without routing or consuming a magic state. Track last_completed_basis per qubit (Some(basis) after single-qubit non-Clifford products; None after Cliffords or multi-qubit products, since joint measurements leave qubits entangled rather than in individual eigenstates). Check after apply_jit_s_correction in sched_remaining; handle dropped products in complete_lcycle via dropped_products_this_lcycle buffer for purging, child unlocking, lcycle_scheduled, and scheduled_products. --- src/scheduler.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index af87f4b..337faef 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -218,6 +218,13 @@ pub(crate) struct Scheduler { runtime_s_parity: Vec, /// Per-product flag; true = operators have already been patched by JIT S correction. jit_corrected: Vec, + /// Per-qubit last-completed measurement basis; None until the first measurement + /// completes or after a Clifford completes on that qubit (Clifford invalidates it). + last_completed_basis: Vec>, + /// Products deterministically dropped this lcycle (same measurement as the preceding + /// completed measurement on the same qubit(s)). + dropped_products_this_lcycle: Vec, + pub(crate) n_dropped_products: usize, } impl Scheduler { @@ -280,6 +287,9 @@ impl Scheduler { unbounded_weight_mode, runtime_s_parity: vec![false; n_circuit_qubits], jit_corrected: vec![false; n_circuit_products], + last_completed_basis: vec![None; n_circuit_qubits], + dropped_products_this_lcycle: Vec::new(), + n_dropped_products: 0, } } @@ -683,7 +693,7 @@ impl Scheduler { n_avail_magic, plotting, ); - if self.pp_paths.is_empty() { + if self.pp_paths.is_empty() && self.dropped_products_this_lcycle.is_empty() { if n_avail_magic > 0 { panic!( "{}", @@ -818,13 +828,67 @@ impl Scheduler { } } + /// Returns true if `pp_id` is a single-qubit non-Clifford product whose + /// (JIT-corrected) operator matches the last completed measurement on that qubit. + /// Such a product is deterministic — its outcome equals the prior measurement's + /// — and can be skipped without physical routing. + /// + /// Restricted to single-qubit products because a joint (multi-qubit) measurement + /// leaves qubits entangled; individual qubit states are not in definite eigenstates + /// afterward, so a later single-qubit measurement on one of those qubits would not + /// be deterministic relative to just that qubit's prior basis. + fn can_drop_product(&self, pp_id: i32) -> bool { + let pp = self.input.circuit.product(pp_id); + if pp.gate_type.is_clifford() || pp.operators.len() != 1 { + return false; + } + let op = &pp.operators[0]; + self.last_completed_basis[op.qubit as usize] == Some(op.basis) + } + + /// Updates `last_completed_basis` after a product completes. + /// + /// Single-qubit non-Clifford products record `Some(basis)` — the qubit is in a + /// definite eigenstate afterward. Everything else (Clifford, multi-qubit) records + /// `None` for each qubit it touches: Cliffords change the qubit's state, and + /// multi-qubit measurements leave qubits entangled rather than in individual + /// eigenstates. + fn update_last_completed_basis(&mut self, pp_id: i32) { + let pp = self.input.circuit.product(pp_id); + let ops: Vec<_> = pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); + if !pp.gate_type.is_clifford() && ops.len() == 1 { + self.last_completed_basis[ops[0].0 as usize] = Some(ops[0].1); + } else { + for (qubit, _) in ops { + self.last_completed_basis[qubit as usize] = None; + } + } + } + /// Second pass: greedily schedule T gates, measurements, and S/SX gates. /// T gates are skipped when no magic state is available. fn sched_remaining(&mut self, n_avail_magic: &mut usize, plotting: bool) { let _timer = accum_start!(self.timers); + // Sync last_completed_basis for products already in pp_paths this lcycle + // (carry-forward recovery T gates and precomputed Clifford trees) before + // checking drops, so a Clifford in pp_paths correctly invalidates its qubit. + for i in 0..self.pp_paths.len() { + let pp_id = self.pp_paths[i].0; + self.update_last_completed_basis(pp_id); + } for i in 0..self.pps_pending.len() { let pp_id = self.pps_pending[i].id; self.apply_jit_s_correction(pp_id); + if self.can_drop_product(pp_id) { + info_sched!( + " Product {} dropped (deterministic: basis matches last completed)", + pp_id + ); + self.n_dropped_products += 1; + self.update_last_completed_basis(pp_id); + self.dropped_products_this_lcycle.push(pp_id); + continue; + } let pp = self.input.circuit.product(pp_id); // Skip if this product would normally use a precomputed Clifford tree AND // the tree is still valid. If JIT correction evicted the tree, fall through @@ -995,10 +1059,12 @@ impl Scheduler { let _timer = accum_start!(self.timers); self.scheduled_ids_buf.clear(); self.scheduled_ids_buf.extend(self.pp_paths.iter().map(|(id, _)| *id)); + // Include dropped products so they are purged from pps_pending. + self.scheduled_ids_buf.extend(self.dropped_products_this_lcycle.iter().copied()); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); // Only count T gates that are newly scheduled (not bounded-mode recovery lcycles) to - // keep the pool-size estimate accurate. + // keep the pool-size estimate accurate. Dropped T gates also don't need future magic. let t_newly_scheduled = self .pp_paths .iter() @@ -1007,11 +1073,39 @@ impl Scheduler { && !self.failed_t_paths.contains_key(id) }) .count(); + let t_dropped = self + .dropped_products_this_lcycle + .iter() + .filter(|&&id| self.input.circuit.product(id).gate_type.is_t()) + .count(); self.cultivation.t_products_remaining = - self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); + self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled + t_dropped); let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); self.recovery_t_ids = t_recovery_ids; self.unlock_children(&t_failed_ids); + // Unlock children of dropped products (they complete without routing this lcycle). + for &pp_id in &self.dropped_products_this_lcycle { + let children: Vec = self.input.circuit.product(pp_id).children.clone(); + for child_id in children { + self.remaining_parents[child_id as usize] -= 1; + if self.remaining_parents[child_id as usize] == 0 + && !self.children_buf.contains(&child_id) + { + self.children_buf.push(child_id); + } + } + } + // Update last_completed_basis for all products that completed this lcycle. + // Dropped products were already updated in sched_remaining(). + let completed_pp_ids: Vec = self + .pp_paths + .iter() + .filter(|(id, _)| !t_failed_ids.contains(id)) + .map(|(id, _)| *id) + .collect(); + for pp_id in completed_pp_ids { + self.update_last_completed_basis(pp_id); + } self.advance_clifford_state(); debug_sched!( "After inserting previous lcycle cliffords, pps_to_sched len {}", @@ -1029,6 +1123,7 @@ impl Scheduler { .iter() .filter(|(id, _)| !t_failed_ids.contains(id)) .map(|(id, _)| *id) + .chain(self.dropped_products_this_lcycle.iter().copied()) .collect(); self.lcycle_scheduled.push((self.current_lcycle, lcycle_ids)); #[cfg(debug_assertions)] @@ -1036,6 +1131,8 @@ impl Scheduler { self.scheduled_products.extend( self.pp_paths.iter().filter(|(id, _)| !t_failed_ids.contains(id)).map(|(id, _)| *id), ); + self.scheduled_products.extend(self.dropped_products_this_lcycle.iter().copied()); + self.dropped_products_this_lcycle.clear(); Ok(()) } @@ -1219,6 +1316,7 @@ impl Scheduler { println!(" min: {}", min); println!(" max: {}", max); println!("T gate failures: {}/{} ({:.1}%)", self.t_gate_failures, tot_t, fail_pct); + println!("Dropped products: {}/{}", self.n_dropped_products, self.input.circuit.n_products()); println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); From 8d865fd1e2e2f795568e35dad734e9d8f1835685 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 08:01:10 +0000 Subject: [PATCH 09/22] Fix bounded-mode JIT parity reset after T gate recovery After a T gate recovery lcycle in bounded mode, the carry-forward routing physically applies the S correction as part of the retry path. The qubit is restored to the same state as a first-attempt success. Clear runtime_s_parity for the recovered qubit so downstream products are not incorrectly JIT-corrected on top of the already-applied physical correction. Detected by tracing the bounded circuit qft_N016 with --log-scheduler info: pp5 (1Y) was being JIT-corrected Y->X after pp3's recovery + pp4 (circuit S gate), putting it in the X terminal while the qubit was in a Y eigenstate. Also demote last_completed_basis update logging from info to debug level. --- src/scheduler.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 337faef..b0e63f3 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -857,10 +857,18 @@ impl Scheduler { let pp = self.input.circuit.product(pp_id); let ops: Vec<_> = pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); if !pp.gate_type.is_clifford() && ops.len() == 1 { + debug_sched!( + " last_completed_basis[{}] = Some({}) (product {})", + ops[0].0, ops[0].1, pp_id + ); self.last_completed_basis[ops[0].0 as usize] = Some(ops[0].1); } else { - for (qubit, _) in ops { - self.last_completed_basis[qubit as usize] = None; + for (qubit, _) in &ops { + debug_sched!( + " last_completed_basis[{}] = None (product {} invalidates)", + qubit, pp_id + ); + self.last_completed_basis[*qubit as usize] = None; } } } @@ -1156,6 +1164,16 @@ impl Scheduler { if self.failed_t_paths.contains_key(&pp_id) { t_recovery_ids.push(pp_id); info_sched!(" T gate {} recovery lcycle succeeded", pp_id); + // Bounded mode only: the carry-forward routing physically applied the S + // correction as part of the retry path. The qubit is now in the same state + // as a first-attempt success. Clear the pending parity so subsequent + // products are not incorrectly JIT-corrected on top of the physical fix. + if !self.unbounded_weight_mode { + let ops = self.input.circuit.product(pp_id).operators.clone(); + for op in ops { + self.runtime_s_parity[op.qubit as usize] = false; + } + } } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { info_sched!(" T gate {} succeeded on first attempt", pp_id); } else { From fea58083e8cfa80f5dac486b41d15e46c6cbacdd Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 21:51:12 -1000 Subject: [PATCH 10/22] updated plotting script to use correct text for volume estimates --- data_processing/plot_puremagic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/data_processing/plot_puremagic.py b/data_processing/plot_puremagic.py index cdf253a..83836dd 100755 --- a/data_processing/plot_puremagic.py +++ b/data_processing/plot_puremagic.py @@ -216,7 +216,7 @@ def _flush(): cur["avg_cultivation_time"] = float(m.group(1)) in_cultivation_block = False - if m := re.match(r"Min volume estimate:\s+(\d+)", s): + if m := re.match(r"Volume estimate:\s+(\d+)", s): cur["min_volume"] = int(m.group(1)) if m := re.match(r"Products per layer:\s+([0-9.eE+\-]+)\s+avg", s): @@ -348,6 +348,8 @@ def prettify_circuit_name(name): # separate trailing _n or _N qubit count: foo_n8 -> foo(8) name = re.sub(r"[_-][nN](\d+)$", lambda mo: f"({mo.group(1)})", name) + name = re.sub(r"(?i)hubbard_18", "hubbard(18)", name) + return name @@ -667,7 +669,7 @@ def main(): action="store_true", default=False, help=( - "Read the 'Min volume estimate' line from each data file and plot it as " + "Read the 'Volume estimate' line from each data file and plot it as " "an additional dashed series alongside each regular volume series. " "Only meaningful when -y is 'volume'." ), @@ -952,7 +954,9 @@ def load_series(y_keys_for_axis, label_suffix=None): def draw_series(ax, series_list, yk_list, colour_offset=0): y_key = yk_list[0] if isinstance(yk_list, list) else yk_list draw_lines = args.lines or args.lines_with_markers - show_markers = args.lines_with_markers or is_cultivation_x or is_weight_x + show_markers = args.lines_with_markers or ( + (is_cultivation_x or is_weight_x) and not args.lines + ) is_timing_y = y_key == "timing" is_total_qubits_y = y_key == "total_qubits" is_ancilla_qubits_y = "ancilla_qubits" in ( @@ -1276,7 +1280,7 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): if d1.empty: minvol_series.append(None) continue - mv_label = f"{file_label} Min volume" if file_label else "Min volume" + mv_label = f"{file_label} Volume" if file_label else "Volume" mv_ys = d1["min_volume"].tolist() if args.max_efficiency: mv_ys = [1.0 / v if v else float("nan") for v in mv_ys] From 50d94b0d82cf7e7922a6ca5f955610a6c9745c52 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 20:06:12 +0000 Subject: [PATCH 11/22] Fix spurious product drops after T gate failures A T gate failure toggles runtime_s_parity (pending JIT S correction) and previously also set last_completed_basis[q] = Some(basis) via the normal update_last_completed_basis path. This caused a cascade of wrong drops: the JIT correction flipped the next T gate's basis, which then matched the failed gate's recorded basis, triggering a drop that treated the measurement as deterministic success. The S correction is a rigid rotation of the entire Bloch sphere: every subsequent axis rotates by the same amount, so the relative orientations between adjacent measurements are preserved. A well-compiled circuit has no two adjacent same-axis T gates, and this rotation cannot create any. There is therefore no valid drop opportunity immediately following a no-retry failure. Fix: track no-retry failed T gate IDs separately in process_t_gate_outcomes and, in complete_lcycle, exclude them from update_last_completed_basis while explicitly setting last_completed_basis[q] = None. Drops after a subsequent success are still valid and still fire (that path goes through normal update_last_completed_basis on the successful gate). Also in this commit: - Remove unbounded_weight_mode flag; replace with per-T-gate Clifford child check so the retry-vs-JIT decision is made per gate - Track t_gate_retries count; print retry and drop percentages in stats - Add Clifford eigenstate propagation in update_last_completed_basis for single-qubit S/SX gates (S: X<->Y, Z->Z; SX: Y<->Z, X->X) to enable valid drops in T->Clifford->T sequences - Remove stale bounded/unbounded mode tests and comments --- src/scheduler.rs | 162 +++++++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 75 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index b0e63f3..8e01ee0 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -6,7 +6,7 @@ use crate::debug_sched; use crate::fn_timer; use crate::info_sched; use crate::node::NodeType; -use crate::pauliproduct::{Operator, PauliProduct}; +use crate::pauliproduct::{GateType, Operator, PauliProduct}; use crate::steinertree::SteinerTree; use crate::topograph::TopoGraph; use crate::treegraph::TreeGraph; @@ -186,6 +186,7 @@ pub(crate) struct Scheduler { /// T gates that failed the 50% coin flip; held for one recovery lcycle. failed_t_paths: IndexMap, Option>)>, pub(crate) t_gate_failures: usize, + pub(crate) t_gate_retries: usize, pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, @@ -210,11 +211,8 @@ pub(crate) struct Scheduler { current_lcycle: usize, /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). recovery_t_ids: Vec, - /// True when the circuit has no Clifford products (all Cliffords absorbed into Pauli frame). - /// Computed once at construction; controls whether JIT S correction is active. - unbounded_weight_mode: bool, /// Per-qubit accumulated S correction parity; true = odd number of pending corrections. - /// Only used in unbounded-weight mode. + /// Toggled on T gate failure when no Clifford child needs the carry-forward retry. runtime_s_parity: Vec, /// Per-product flag; true = operators have already been patched by JIT S correction. jit_corrected: Vec, @@ -248,7 +246,6 @@ impl Scheduler { let n_nodes = topo.n_nodes; let n_circuit_qubits = circuit.n_qubits; let n_circuit_products = circuit.n_products(); - let unbounded_weight_mode = !circuit.pps.iter().any(|pp| pp.gate_type.is_clifford()); let mut timers = AccumTimers::new(); let loop_timer = timers.add_or_get("schedule loop"); let other_timer = timers.add_or_get("other "); @@ -265,6 +262,7 @@ impl Scheduler { clifford_paths: IndexMap::new(), failed_t_paths: IndexMap::new(), t_gate_failures: 0, + t_gate_retries: 0, stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, @@ -284,7 +282,6 @@ impl Scheduler { pp_paths: Vec::new(), current_lcycle: 0, recovery_t_ids: Vec::new(), - unbounded_weight_mode, runtime_s_parity: vec![false; n_circuit_qubits], jit_corrected: vec![false; n_circuit_products], last_completed_basis: vec![None; n_circuit_qubits], @@ -436,8 +433,8 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // S corrections are absorbed into the Pauli frame via JIT correction in both - // bounded and unbounded weight modes; no extra physical S products are scheduled. + // S corrections are absorbed into the Pauli frame via JIT correction; + // no extra physical S products are ever scheduled. Ok((self.current_lcycle, self.scheduled_products.len())) } @@ -782,9 +779,8 @@ impl Scheduler { /// then recomputes routing caches if any basis changed (X↔Y). /// /// Only runs once per product (guarded by `jit_corrected`) so retried T gates - /// do not get double-corrected. Clifford products are skipped — their S - /// correction is handled by the bounded-mode carry-forward routing — but the - /// parity flag is left set so it propagates to the next non-Clifford product. + /// do not get double-corrected. Clifford products are skipped — parity carries + /// forward past them to the next non-Clifford product. /// /// If a basis change occurs, also evicts any stale precomputed Clifford tree for /// the product — the tree was built for the old operator bases and is now wrong. @@ -848,11 +844,10 @@ impl Scheduler { /// Updates `last_completed_basis` after a product completes. /// - /// Single-qubit non-Clifford products record `Some(basis)` — the qubit is in a - /// definite eigenstate afterward. Everything else (Clifford, multi-qubit) records - /// `None` for each qubit it touches: Cliffords change the qubit's state, and - /// multi-qubit measurements leave qubits entangled rather than in individual - /// eigenstates. + /// Single-qubit non-Clifford products record `Some(basis)`. Single-qubit S/SX + /// propagate the known eigenstate through the conjugation: S: X↔Y, Z→Z; + /// SX: Y↔Z, X→X. Everything else (CX, multi-qubit) records `None`: multi-qubit + /// measurements leave qubits entangled, and CX entangles both qubits. fn update_last_completed_basis(&mut self, pp_id: i32) { let pp = self.input.circuit.product(pp_id); let ops: Vec<_> = pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); @@ -862,6 +857,24 @@ impl Scheduler { ops[0].0, ops[0].1, pp_id ); self.last_completed_basis[ops[0].0 as usize] = Some(ops[0].1); + } else if ops.len() == 1 + && (pp.gate_type == GateType::S || pp.gate_type == GateType::SX) + { + let gate_type = pp.gate_type; + let qubit = ops[0].0 as usize; + self.last_completed_basis[qubit] = self.last_completed_basis[qubit].map(|b| { + match (gate_type, b) { + (GateType::S, 'X') => 'Y', + (GateType::S, 'Y') => 'X', + (GateType::SX, 'Y') => 'Z', + (GateType::SX, 'Z') => 'Y', + _ => b, + } + }); + debug_sched!( + " last_completed_basis[{}] = {:?} (product {} Clifford propagate)", + qubit, self.last_completed_basis[qubit], pp_id + ); } else { for (qubit, _) in &ops { debug_sched!( @@ -1071,7 +1084,7 @@ impl Scheduler { self.scheduled_ids_buf.extend(self.dropped_products_this_lcycle.iter().copied()); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); - // Only count T gates that are newly scheduled (not bounded-mode recovery lcycles) to + // Only count T gates that are newly scheduled (not carry-forward retry lcycles) to // keep the pool-size estimate accurate. Dropped T gates also don't need future magic. let t_newly_scheduled = self .pp_paths @@ -1088,7 +1101,7 @@ impl Scheduler { .count(); self.cultivation.t_products_remaining = self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled + t_dropped); - let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); + let (t_failed_ids, t_recovery_ids, t_no_retry_failed_ids) = self.process_t_gate_outcomes(); self.recovery_t_ids = t_recovery_ids; self.unlock_children(&t_failed_ids); // Unlock children of dropped products (they complete without routing this lcycle). @@ -1103,12 +1116,24 @@ impl Scheduler { } } } - // Update last_completed_basis for all products that completed this lcycle. + // A no-retry T gate failure leaves the qubit in the negative eigenstate of the + // measured basis: the JIT parity flip is a rigid rotation of all subsequent axes, + // so no adjacent measurement can become collinear. Invalidate the eigenstate so + // subsequent products are never spuriously dropped. + for &pp_id in &t_no_retry_failed_ids { + let ops: Vec<_> = self.input.circuit.product(pp_id).operators.iter() + .map(|op| op.qubit as usize).collect(); + for qubit in ops { + self.last_completed_basis[qubit] = None; + } + } + // Update last_completed_basis for all products that genuinely completed this lcycle. // Dropped products were already updated in sched_remaining(). + // No-retry failures are excluded above — their eigenstate is invalidated instead. let completed_pp_ids: Vec = self .pp_paths .iter() - .filter(|(id, _)| !t_failed_ids.contains(id)) + .filter(|(id, _)| !t_failed_ids.contains(id) && !t_no_retry_failed_ids.contains(id)) .map(|(id, _)| *id) .collect(); for pp_id in completed_pp_ids { @@ -1144,18 +1169,17 @@ impl Scheduler { Ok(()) } - /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids). + /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids, no_retry_failed_ids). /// - /// **Unbounded-weight mode**: a "failed" T gate (50% coin) is treated as complete in this - /// lcycle — the S correction is absorbed into `runtime_s_parity` and children are unlocked - /// normally. No retry, no extra magic state consumed. Only the parity flag changes. - /// - /// **Bounded-weight mode**: parity is also toggled (JIT correction for non-Clifford - /// descendants), and the failed gate is stored in `failed_t_paths` with the magic root - /// trimmed off so the routing subtree can be carried forward to a recovery lcycle. - fn process_t_gate_outcomes(&mut self) -> (Vec, Vec) { + /// On failure, parity is always toggled for the affected qubit(s). If any immediate child + /// is a Clifford product, the T gate is retried via carry-forward routing (stored in + /// `failed_t_paths`) and parity is reset on recovery — the carry-forward physically applies + /// the S correction. If all children are non-Clifford, no retry is needed; JIT parity + /// correction handles the S correction when each child is scheduled. + fn process_t_gate_outcomes(&mut self) -> (Vec, Vec, Vec) { let mut t_failed_ids: Vec = Vec::new(); let mut t_recovery_ids: Vec = Vec::new(); + let mut t_no_retry_failed_ids: Vec = Vec::new(); let pp_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); for pp_id in &pp_ids { let pp_id = *pp_id; @@ -1164,49 +1188,46 @@ impl Scheduler { if self.failed_t_paths.contains_key(&pp_id) { t_recovery_ids.push(pp_id); info_sched!(" T gate {} recovery lcycle succeeded", pp_id); - // Bounded mode only: the carry-forward routing physically applied the S - // correction as part of the retry path. The qubit is now in the same state - // as a first-attempt success. Clear the pending parity so subsequent - // products are not incorrectly JIT-corrected on top of the physical fix. - if !self.unbounded_weight_mode { - let ops = self.input.circuit.product(pp_id).operators.clone(); - for op in ops { - self.runtime_s_parity[op.qubit as usize] = false; - } + // The carry-forward routing physically applied the S correction, so the + // qubit is in the same state as a first-attempt success. Clear parity so + // subsequent non-Clifford products are not JIT-corrected on top. + let ops = self.input.circuit.product(pp_id).operators.clone(); + for op in ops { + self.runtime_s_parity[op.qubit as usize] = false; } } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { info_sched!(" T gate {} succeeded on first attempt", pp_id); } else { self.t_gate_failures += 1; - if self.unbounded_weight_mode { - // S correction absorbed into Pauli frame: toggle parity and complete. - // No retry — the T gate finishes this lcycle like a success. + // Always toggle parity for the failing qubit(s). + let ops: Vec<_> = pp.operators.clone(); + for op in &ops { + self.runtime_s_parity[op.qubit as usize] ^= true; + } + // Retry (carry-forward) only when a Clifford child needs physical routing + // of the S correction. Non-Clifford children are handled by JIT parity. + let has_clifford_child = self.input.circuit.product(pp_id) + .children.iter() + .any(|&cid| self.input.circuit.product(cid).gate_type.is_clifford()); + if has_clifford_child { + t_failed_ids.push(pp_id); + self.t_gate_retries += 1; info_sched!( - " T gate {} S-corrected (parity toggled, no retry)", + " T gate {} failed, retry needed (Clifford child)", pp_id ); - let ops: Vec<_> = pp.operators.clone(); - for op in ops { - self.runtime_s_parity[op.qubit as usize] ^= true; - } } else { - t_failed_ids.push(pp_id); info_sched!( - " T gate {} failed (50% probability), recovery lcycle next", + " T gate {} S-corrected (parity toggled, no retry)", pp_id ); - // Toggle parity so JIT correction applies to subsequent - // non-Clifford products; carry-forward routing handles Clifford products. - let ops: Vec<_> = pp.operators.clone(); - for op in ops { - self.runtime_s_parity[op.qubit as usize] ^= true; - } + t_no_retry_failed_ids.push(pp_id); } } } } - // Bounded mode only: update failed_t_paths for retry routing. - if !self.unbounded_weight_mode { + // Update failed_t_paths: insert new retry entries, remove recovered ones. + if !t_failed_ids.is_empty() || !self.failed_t_paths.is_empty() { let pp_paths_snapshot: Vec<(i32, Option>)> = self .pp_paths .iter() @@ -1239,7 +1260,7 @@ impl Scheduler { } } } - (t_failed_ids, t_recovery_ids) + (t_failed_ids, t_recovery_ids, t_no_retry_failed_ids) } /// Decrements `remaining_parents` for each completed product and collects @@ -1334,7 +1355,14 @@ impl Scheduler { println!(" min: {}", min); println!(" max: {}", max); println!("T gate failures: {}/{} ({:.1}%)", self.t_gate_failures, tot_t, fail_pct); - println!("Dropped products: {}/{}", self.n_dropped_products, self.input.circuit.n_products()); + let retry_pct = if self.t_gate_failures > 0 { + 100.0 * self.t_gate_retries as f64 / self.t_gate_failures as f64 + } else { 0.0 }; + let drop_pct = if self.input.circuit.n_products() > 0 { + 100.0 * self.n_dropped_products as f64 / self.input.circuit.n_products() as f64 + } else { 0.0 }; + println!("T gate retries (Clifford child): {}/{} ({:.1}%)", self.t_gate_retries, self.t_gate_failures, retry_pct); + println!("Dropped products: {}/{} ({:.1}%)", self.n_dropped_products, self.input.circuit.n_products(), drop_pct); println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); @@ -1749,22 +1777,6 @@ mod tests { ); } - #[test] - fn unbounded_mode_detected_when_no_clifford_products() { - // A circuit with only T products has no Clifford products → unbounded mode. - let lines = &["+X___", "-_X__"]; - let sched = run_scheduler(lines, 0); - assert!(sched.unbounded_weight_mode); - } - - #[test] - fn bounded_mode_detected_with_s_product() { - // A circuit with an S product is NOT in unbounded mode. - let lines = &["+X___", "+X___", "-_X__"]; - let sched = run_scheduler(lines, 0); - assert!(!sched.unbounded_weight_mode); - } - #[test] fn scheduler_completes_with_jit_correction_enabled() { // Two independent T gates on separate qubits; any failures should be From 05c2e37c3c0924199176f70b1e8f18749a71747b Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 20:22:35 +0000 Subject: [PATCH 12/22] Revert scheduler.rs and puremagic.rs to 343727c --- src/puremagic.rs | 4 - src/scheduler.rs | 502 +++++++++-------------------------------------- 2 files changed, 91 insertions(+), 415 deletions(-) diff --git a/src/puremagic.rs b/src/puremagic.rs index e20fa9b..9675c6d 100644 --- a/src/puremagic.rs +++ b/src/puremagic.rs @@ -67,9 +67,6 @@ struct Args { /// Disable T gate failures (every T gate succeeds on first attempt) #[arg(short = 'F', long)] no_t_failures: bool, - /// Record normalized cultivation-time distribution to .cultivation_dist - #[arg(short = 'C', long)] - record_cultivation_dist: bool, /// Number of ancilla between each data patch (all magic routing only) #[arg(short, long, default_value = "1")] ancilla_rows: usize, @@ -149,7 +146,6 @@ fn main() -> Result<(), Box> { args.plot.join(" "), args.rseed, args.no_t_failures, - args.record_cultivation_dist, ); let (tot_lcycles, n_scheduled) = sched.sched_circuit()?; diff --git a/src/scheduler.rs b/src/scheduler.rs index 8e01ee0..7967f47 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -6,7 +6,7 @@ use crate::debug_sched; use crate::fn_timer; use crate::info_sched; use crate::node::NodeType; -use crate::pauliproduct::{GateType, Operator, PauliProduct}; +use crate::pauliproduct::{Operator, PauliProduct}; use crate::steinertree::SteinerTree; use crate::topograph::TopoGraph; use crate::treegraph::TreeGraph; @@ -186,11 +186,9 @@ pub(crate) struct Scheduler { /// T gates that failed the 50% coin flip; held for one recovery lcycle. failed_t_paths: IndexMap, Option>)>, pub(crate) t_gate_failures: usize, - pub(crate) t_gate_retries: usize, pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, - record_cultivation_dist: bool, /// Reusable buffers to avoid per-lcycle allocations. terminals_buf: Vec, scheduled_ids_buf: Vec, @@ -211,24 +209,12 @@ pub(crate) struct Scheduler { current_lcycle: usize, /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). recovery_t_ids: Vec, - /// Per-qubit accumulated S correction parity; true = odd number of pending corrections. - /// Toggled on T gate failure when no Clifford child needs the carry-forward retry. - runtime_s_parity: Vec, - /// Per-product flag; true = operators have already been patched by JIT S correction. - jit_corrected: Vec, - /// Per-qubit last-completed measurement basis; None until the first measurement - /// completes or after a Clifford completes on that qubit (Clifford invalidates it). - last_completed_basis: Vec>, - /// Products deterministically dropped this lcycle (same measurement as the preceding - /// completed measurement on the same qubit(s)). - dropped_products_this_lcycle: Vec, - pub(crate) n_dropped_products: usize, } impl Scheduler { pub(crate) fn new( circuit: Circuit, topo: TopoGraph, magic_state_lambda: f64, log_level: &str, - plot_option: String, rseed: u32, no_t_failures: bool, record_cultivation_dist: bool, + plot_option: String, rseed: u32, no_t_failures: bool, ) -> Self { if log_level != "none" { let trace_fname = format!("{}.sched_trace", circuit_stem(&circuit.circuit_fname)); @@ -244,8 +230,6 @@ impl Scheduler { let n_bus_qubits = topo.n_bus_qubits; let n_magic_qubits = topo.n_magic_qubits; let n_nodes = topo.n_nodes; - let n_circuit_qubits = circuit.n_qubits; - let n_circuit_products = circuit.n_products(); let mut timers = AccumTimers::new(); let loop_timer = timers.add_or_get("schedule loop"); let other_timer = timers.add_or_get("other "); @@ -262,11 +246,9 @@ impl Scheduler { clifford_paths: IndexMap::new(), failed_t_paths: IndexMap::new(), t_gate_failures: 0, - t_gate_retries: 0, stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, - record_cultivation_dist, terminals_buf: Vec::new(), scheduled_ids_buf: Vec::new(), children_buf: Vec::new(), @@ -282,15 +264,9 @@ impl Scheduler { pp_paths: Vec::new(), current_lcycle: 0, recovery_t_ids: Vec::new(), - runtime_s_parity: vec![false; n_circuit_qubits], - jit_corrected: vec![false; n_circuit_products], - last_completed_basis: vec![None; n_circuit_qubits], - dropped_products_this_lcycle: Vec::new(), - n_dropped_products: 0, } } - pub(crate) fn count_t_products(&self) -> usize { (0..self.input.circuit.n_products()) .filter(|&id| self.input.circuit.product(id as i32).gate_type.is_t()) @@ -433,9 +409,9 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // S corrections are absorbed into the Pauli frame via JIT correction; - // no extra physical S products are ever scheduled. - Ok((self.current_lcycle, self.scheduled_products.len())) + // Return total lcycles and total "attempts" (successes + failures), so the + // caller can compute the true T-gate count including failed first attempts. + Ok((self.current_lcycle, self.scheduled_products.len() + self.t_gate_failures)) } fn init_magic_nodes(&mut self) { @@ -578,53 +554,47 @@ impl Scheduler { self.precomputed_terminals = vec![Vec::new(); n_products]; self.precomputed_root_info = vec![Vec::new(); n_products]; for pp_id in 0..n_products { - self.recompute_terminals_for_product(pp_id); - } - println!("Precomputed terminals and root candidates for {} products", n_products); - } - - /// Recomputes terminal node IDs and root candidates for a single product. - /// Called once for all products at startup and again when a JIT S correction - /// changes a product's operator bases (X↔Y), altering its terminal set. - fn recompute_terminals_for_product(&mut self, pp_id: usize) { - let pp = self.input.circuit.product(pp_id as i32).clone(); - let terminals = operators_to_node_ids(&self.input.topo, &pp.operators); - let mut root_info: Vec<(bool, Vec, Vec)> = Vec::with_capacity(terminals.len()); - for &term_id in &terminals { - let node = self.input.topo.node(term_id); - // is_paired: this terminal's paired data node is also a terminal - // (i.e. the operator is Y-basis, producing both X and Z data nodes). - let is_paired = - node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); - let mut preferred: Vec = Vec::new(); - let mut side: Vec = Vec::new(); - if is_paired { - // X nodes look downward (toward paired Z), Z nodes look upward. - let is_x = self.input.topo.label(term_id).contains('X'); - for &nb_id in node.nbs_slice() { - let nb = self.input.topo.node(nb_id); - if !nb.is_routing() { - continue; - } - if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { - preferred.push(nb_id); - } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { - side.push(nb_id); + let pp = self.input.circuit.product(pp_id as i32).clone(); + let terminals = operators_to_node_ids(&self.input.topo, &pp.operators); + let mut root_info: Vec<(bool, Vec, Vec)> = + Vec::with_capacity(terminals.len()); + for &term_id in &terminals { + let node = self.input.topo.node(term_id); + // is_paired: this terminal's paired data node is also a terminal + // (i.e. the operator is Y-basis, producing both X and Z data nodes). + let is_paired = + node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); + let mut preferred: Vec = Vec::new(); + let mut side: Vec = Vec::new(); + if is_paired { + // X nodes look downward (toward paired Z), Z nodes look upward. + let is_x = self.input.topo.label(term_id).contains('X'); + for &nb_id in node.nbs_slice() { + let nb = self.input.topo.node(nb_id); + if !nb.is_routing() { + continue; + } + if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { + preferred.push(nb_id); + } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + side.push(nb_id); + } } - } - } else { - // Unpaired terminal: only same-row side nbs are valid roots. - for &nb_id in node.nbs_slice() { - let nb = self.input.topo.node(nb_id); - if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { - preferred.push(nb_id); + } else { + // Unpaired terminal: only same-row side nbs are valid roots. + for &nb_id in node.nbs_slice() { + let nb = self.input.topo.node(nb_id); + if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + preferred.push(nb_id); + } } } + root_info.push((is_paired, preferred, side)); } - root_info.push((is_paired, preferred, side)); + self.precomputed_terminals[pp_id] = terminals; + self.precomputed_root_info[pp_id] = root_info; } - self.precomputed_terminals[pp_id] = terminals; - self.precomputed_root_info[pp_id] = root_info; + println!("Precomputed terminals and root candidates for {} products", n_products); } fn mark_nodes_used(&mut self, node_ids: &[u16]) { @@ -690,7 +660,7 @@ impl Scheduler { n_avail_magic, plotting, ); - if self.pp_paths.is_empty() && self.dropped_products_this_lcycle.is_empty() { + if self.pp_paths.is_empty() { if n_avail_magic > 0 { panic!( "{}", @@ -733,10 +703,6 @@ impl Scheduler { // before the loop body calls `&mut self` methods. let ids_to_process: Vec = self.remaining_ids_buf.clone(); for &pp_id in &ids_to_process { - // Apply pending S corrections before consulting the precomputed tree. - // If any basis changed, the stale tree is evicted inside apply_jit_s_correction - // and the product will fall through to sched_remaining for fresh routing. - self.apply_jit_s_correction(pp_id); // Clone the Rc to end the immutable borrow on `precomputed_clifford_trees`. let Some(tree) = self.precomputed_clifford_trees.get(&pp_id).map(Rc::clone) else { continue; @@ -775,146 +741,14 @@ impl Scheduler { } } - /// Applies any pending S corrections to product `pp_id`'s operators in-place, - /// then recomputes routing caches if any basis changed (X↔Y). - /// - /// Only runs once per product (guarded by `jit_corrected`) so retried T gates - /// do not get double-corrected. Clifford products are skipped — parity carries - /// forward past them to the next non-Clifford product. - /// - /// If a basis change occurs, also evicts any stale precomputed Clifford tree for - /// the product — the tree was built for the old operator bases and is now wrong. - /// The product will then fall through to `sched_remaining()` for fresh routing. - fn apply_jit_s_correction(&mut self, pp_id: i32) { - if self.jit_corrected[pp_id as usize] { - return; - } - // Clifford products: parity carries forward past them. - if self.input.circuit.product(pp_id).gate_type.is_clifford() { - return; - } - self.jit_corrected[pp_id as usize] = true; - let mut needs_recompute = false; - let n_ops = self.input.circuit.product(pp_id).operators.len(); - for i in 0..n_ops { - let qubit = self.input.circuit.product(pp_id).operators[i].qubit; - if !self.runtime_s_parity[qubit as usize] { - continue; - } - let original = self.input.circuit.product(pp_id).operators[i].basis; - let new_basis = match original { - 'X' => 'Y', - 'Y' => 'X', - _ => original, // Z → Z unchanged - }; - if new_basis != original { - self.input.circuit.product_mut(pp_id).operators[i].basis = new_basis; - needs_recompute = true; - info_sched!( - " S correction JIT: product {} qubit {} {} → {}", - pp_id, qubit, original, new_basis - ); - } - } - if needs_recompute { - self.recompute_terminals_for_product(pp_id as usize); - // Any precomputed Clifford tree was built for the old operator bases - // and is now stale; drop it so the product re-routes via sched_remaining. - self.precomputed_clifford_trees.remove(&pp_id); - } - } - - /// Returns true if `pp_id` is a single-qubit non-Clifford product whose - /// (JIT-corrected) operator matches the last completed measurement on that qubit. - /// Such a product is deterministic — its outcome equals the prior measurement's - /// — and can be skipped without physical routing. - /// - /// Restricted to single-qubit products because a joint (multi-qubit) measurement - /// leaves qubits entangled; individual qubit states are not in definite eigenstates - /// afterward, so a later single-qubit measurement on one of those qubits would not - /// be deterministic relative to just that qubit's prior basis. - fn can_drop_product(&self, pp_id: i32) -> bool { - let pp = self.input.circuit.product(pp_id); - if pp.gate_type.is_clifford() || pp.operators.len() != 1 { - return false; - } - let op = &pp.operators[0]; - self.last_completed_basis[op.qubit as usize] == Some(op.basis) - } - - /// Updates `last_completed_basis` after a product completes. - /// - /// Single-qubit non-Clifford products record `Some(basis)`. Single-qubit S/SX - /// propagate the known eigenstate through the conjugation: S: X↔Y, Z→Z; - /// SX: Y↔Z, X→X. Everything else (CX, multi-qubit) records `None`: multi-qubit - /// measurements leave qubits entangled, and CX entangles both qubits. - fn update_last_completed_basis(&mut self, pp_id: i32) { - let pp = self.input.circuit.product(pp_id); - let ops: Vec<_> = pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); - if !pp.gate_type.is_clifford() && ops.len() == 1 { - debug_sched!( - " last_completed_basis[{}] = Some({}) (product {})", - ops[0].0, ops[0].1, pp_id - ); - self.last_completed_basis[ops[0].0 as usize] = Some(ops[0].1); - } else if ops.len() == 1 - && (pp.gate_type == GateType::S || pp.gate_type == GateType::SX) - { - let gate_type = pp.gate_type; - let qubit = ops[0].0 as usize; - self.last_completed_basis[qubit] = self.last_completed_basis[qubit].map(|b| { - match (gate_type, b) { - (GateType::S, 'X') => 'Y', - (GateType::S, 'Y') => 'X', - (GateType::SX, 'Y') => 'Z', - (GateType::SX, 'Z') => 'Y', - _ => b, - } - }); - debug_sched!( - " last_completed_basis[{}] = {:?} (product {} Clifford propagate)", - qubit, self.last_completed_basis[qubit], pp_id - ); - } else { - for (qubit, _) in &ops { - debug_sched!( - " last_completed_basis[{}] = None (product {} invalidates)", - qubit, pp_id - ); - self.last_completed_basis[*qubit as usize] = None; - } - } - } - /// Second pass: greedily schedule T gates, measurements, and S/SX gates. /// T gates are skipped when no magic state is available. fn sched_remaining(&mut self, n_avail_magic: &mut usize, plotting: bool) { let _timer = accum_start!(self.timers); - // Sync last_completed_basis for products already in pp_paths this lcycle - // (carry-forward recovery T gates and precomputed Clifford trees) before - // checking drops, so a Clifford in pp_paths correctly invalidates its qubit. - for i in 0..self.pp_paths.len() { - let pp_id = self.pp_paths[i].0; - self.update_last_completed_basis(pp_id); - } for i in 0..self.pps_pending.len() { let pp_id = self.pps_pending[i].id; - self.apply_jit_s_correction(pp_id); - if self.can_drop_product(pp_id) { - info_sched!( - " Product {} dropped (deterministic: basis matches last completed)", - pp_id - ); - self.n_dropped_products += 1; - self.update_last_completed_basis(pp_id); - self.dropped_products_this_lcycle.push(pp_id); - continue; - } let pp = self.input.circuit.product(pp_id); - // Skip if this product would normally use a precomputed Clifford tree AND - // the tree is still valid. If JIT correction evicted the tree, fall through - // to route it fresh via Steiner tree. - if Self::should_precompute(pp) && self.precomputed_clifford_trees.contains_key(&pp_id) { + if Self::should_precompute(pp) { continue; } let (pp_id, gate_type) = (pp.id, pp.gate_type); @@ -1048,12 +882,9 @@ impl Scheduler { plotting, ) } else { - // A should_precompute product can reach here if its precomputed Clifford tree - // was evicted by a JIT S correction; route it fresh via Steiner tree. debug_assert!( - !Self::should_precompute(self.input.circuit.product(pp_id)) - || !self.precomputed_clifford_trees.contains_key(&pp_id), - "should_precompute product {:?} reached Steiner path with valid precomputed tree", + !Self::should_precompute(self.input.circuit.product(pp_id)), + "should_precompute product {:?} reached Steiner path", pp_id ); match self.stree_computation.compute( @@ -1080,12 +911,10 @@ impl Scheduler { let _timer = accum_start!(self.timers); self.scheduled_ids_buf.clear(); self.scheduled_ids_buf.extend(self.pp_paths.iter().map(|(id, _)| *id)); - // Include dropped products so they are purged from pps_pending. - self.scheduled_ids_buf.extend(self.dropped_products_this_lcycle.iter().copied()); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); - // Only count T gates that are newly scheduled (not carry-forward retry lcycles) to - // keep the pool-size estimate accurate. Dropped T gates also don't need future magic. + // Only count T gates that are newly scheduled (not recovery lcycles) to + // keep the pool-size estimate accurate. let t_newly_scheduled = self .pp_paths .iter() @@ -1094,51 +923,11 @@ impl Scheduler { && !self.failed_t_paths.contains_key(id) }) .count(); - let t_dropped = self - .dropped_products_this_lcycle - .iter() - .filter(|&&id| self.input.circuit.product(id).gate_type.is_t()) - .count(); self.cultivation.t_products_remaining = - self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled + t_dropped); - let (t_failed_ids, t_recovery_ids, t_no_retry_failed_ids) = self.process_t_gate_outcomes(); + self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); + let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); self.recovery_t_ids = t_recovery_ids; self.unlock_children(&t_failed_ids); - // Unlock children of dropped products (they complete without routing this lcycle). - for &pp_id in &self.dropped_products_this_lcycle { - let children: Vec = self.input.circuit.product(pp_id).children.clone(); - for child_id in children { - self.remaining_parents[child_id as usize] -= 1; - if self.remaining_parents[child_id as usize] == 0 - && !self.children_buf.contains(&child_id) - { - self.children_buf.push(child_id); - } - } - } - // A no-retry T gate failure leaves the qubit in the negative eigenstate of the - // measured basis: the JIT parity flip is a rigid rotation of all subsequent axes, - // so no adjacent measurement can become collinear. Invalidate the eigenstate so - // subsequent products are never spuriously dropped. - for &pp_id in &t_no_retry_failed_ids { - let ops: Vec<_> = self.input.circuit.product(pp_id).operators.iter() - .map(|op| op.qubit as usize).collect(); - for qubit in ops { - self.last_completed_basis[qubit] = None; - } - } - // Update last_completed_basis for all products that genuinely completed this lcycle. - // Dropped products were already updated in sched_remaining(). - // No-retry failures are excluded above — their eigenstate is invalidated instead. - let completed_pp_ids: Vec = self - .pp_paths - .iter() - .filter(|(id, _)| !t_failed_ids.contains(id) && !t_no_retry_failed_ids.contains(id)) - .map(|(id, _)| *id) - .collect(); - for pp_id in completed_pp_ids { - self.update_last_completed_basis(pp_id); - } self.advance_clifford_state(); debug_sched!( "After inserting previous lcycle cliffords, pps_to_sched len {}", @@ -1156,7 +945,6 @@ impl Scheduler { .iter() .filter(|(id, _)| !t_failed_ids.contains(id)) .map(|(id, _)| *id) - .chain(self.dropped_products_this_lcycle.iter().copied()) .collect(); self.lcycle_scheduled.push((self.current_lcycle, lcycle_ids)); #[cfg(debug_assertions)] @@ -1164,22 +952,18 @@ impl Scheduler { self.scheduled_products.extend( self.pp_paths.iter().filter(|(id, _)| !t_failed_ids.contains(id)).map(|(id, _)| *id), ); - self.scheduled_products.extend(self.dropped_products_this_lcycle.iter().copied()); - self.dropped_products_this_lcycle.clear(); Ok(()) } - /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids, no_retry_failed_ids). + /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids). /// - /// On failure, parity is always toggled for the affected qubit(s). If any immediate child - /// is a Clifford product, the T gate is retried via carry-forward routing (stored in - /// `failed_t_paths`) and parity is reset on recovery — the carry-forward physically applies - /// the S correction. If all children are non-Clifford, no retry is needed; JIT parity - /// correction handles the S correction when each child is scheduled. - fn process_t_gate_outcomes(&mut self) -> (Vec, Vec, Vec) { + /// First-attempt T gates succeed with 50% probability (or always if `no_t_failures`). + /// Recovery-lcycle T gates (already in `failed_t_paths`) always succeed. + /// Failed gates are stored in `failed_t_paths` with the magic root trimmed off + /// so the routing subtree can be reused in the recovery lcycle. + fn process_t_gate_outcomes(&mut self) -> (Vec, Vec) { let mut t_failed_ids: Vec = Vec::new(); let mut t_recovery_ids: Vec = Vec::new(); - let mut t_no_retry_failed_ids: Vec = Vec::new(); let pp_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); for pp_id in &pp_ids { let pp_id = *pp_id; @@ -1188,79 +972,46 @@ impl Scheduler { if self.failed_t_paths.contains_key(&pp_id) { t_recovery_ids.push(pp_id); info_sched!(" T gate {} recovery lcycle succeeded", pp_id); - // The carry-forward routing physically applied the S correction, so the - // qubit is in the same state as a first-attempt success. Clear parity so - // subsequent non-Clifford products are not JIT-corrected on top. - let ops = self.input.circuit.product(pp_id).operators.clone(); - for op in ops { - self.runtime_s_parity[op.qubit as usize] = false; - } } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { info_sched!(" T gate {} succeeded on first attempt", pp_id); } else { + t_failed_ids.push(pp_id); self.t_gate_failures += 1; - // Always toggle parity for the failing qubit(s). - let ops: Vec<_> = pp.operators.clone(); - for op in &ops { - self.runtime_s_parity[op.qubit as usize] ^= true; - } - // Retry (carry-forward) only when a Clifford child needs physical routing - // of the S correction. Non-Clifford children are handled by JIT parity. - let has_clifford_child = self.input.circuit.product(pp_id) - .children.iter() - .any(|&cid| self.input.circuit.product(cid).gate_type.is_clifford()); - if has_clifford_child { - t_failed_ids.push(pp_id); - self.t_gate_retries += 1; - info_sched!( - " T gate {} failed, retry needed (Clifford child)", - pp_id - ); - } else { - info_sched!( - " T gate {} S-corrected (parity toggled, no retry)", - pp_id - ); - t_no_retry_failed_ids.push(pp_id); - } + info_sched!( + " T gate {} failed (50% probability), recovery lcycle next", + pp_id + ); } } } - // Update failed_t_paths: insert new retry entries, remove recovered ones. - if !t_failed_ids.is_empty() || !self.failed_t_paths.is_empty() { - let pp_paths_snapshot: Vec<(i32, Option>)> = self - .pp_paths - .iter() - .map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))) - .collect(); - for (pp_id, opt_pp_path) in &pp_paths_snapshot { - let pp_id = *pp_id; - let pp = self.input.circuit.product(pp_id); - if !pp.gate_type.is_t() { - continue; - } - if t_failed_ids.contains(&pp_id) { - // Store the routing subtree without the magic root so the recovery - // lcycle can reuse the same data/routing nodes while finding a new - // magic state. When not plotting, fall back to just the terminal IDs. - let trimmed_opt_tree: Option> = - opt_pp_path.as_ref().map(|tree| { - let mut t = (**tree).clone(); - t.trim_magic_root(); - Rc::new(t) - }); - let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { - trimmed.iter_nodes().collect() - } else { - self.precomputed_terminals[pp_id as usize].clone() - }; - self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); + let pp_paths_snapshot: Vec<(i32, Option>)> = + self.pp_paths.iter().map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))).collect(); + for (pp_id, opt_pp_path) in &pp_paths_snapshot { + let pp_id = *pp_id; + let pp = self.input.circuit.product(pp_id); + if !pp.gate_type.is_t() { + continue; + } + if t_failed_ids.contains(&pp_id) { + // Store the routing subtree without the magic root so the recovery + // lcycle can reuse the same data/routing nodes while finding a new + // magic state. When not plotting, fall back to just the terminal IDs. + let trimmed_opt_tree: Option> = opt_pp_path.as_ref().map(|tree| { + let mut t = (**tree).clone(); + t.trim_magic_root(); + Rc::new(t) + }); + let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { + trimmed.iter_nodes().collect() } else { - self.failed_t_paths.swap_remove(&pp_id); - } + self.precomputed_terminals[pp_id as usize].clone() + }; + self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); + } else { + self.failed_t_paths.swap_remove(&pp_id); } } - (t_failed_ids, t_recovery_ids, t_no_retry_failed_ids) + (t_failed_ids, t_recovery_ids) } /// Decrements `remaining_parents` for each completed product and collects @@ -1355,23 +1106,14 @@ impl Scheduler { println!(" min: {}", min); println!(" max: {}", max); println!("T gate failures: {}/{} ({:.1}%)", self.t_gate_failures, tot_t, fail_pct); - let retry_pct = if self.t_gate_failures > 0 { - 100.0 * self.t_gate_retries as f64 / self.t_gate_failures as f64 - } else { 0.0 }; - let drop_pct = if self.input.circuit.n_products() > 0 { - 100.0 * self.n_dropped_products as f64 / self.input.circuit.n_products() as f64 - } else { 0.0 }; - println!("T gate retries (Clifford child): {}/{} ({:.1}%)", self.t_gate_retries, self.t_gate_failures, retry_pct); - println!("Dropped products: {}/{} ({:.1}%)", self.n_dropped_products, self.input.circuit.n_products(), drop_pct); println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); - if self.record_cultivation_dist { - let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); - match self.write_cultivation_dist(&dist_fname, min, max) { - Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), - Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), - } + // Write normalized cultivation-time distribution to file. + let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); + match self.write_cultivation_dist(&dist_fname, min, max) { + Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), + Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), } } @@ -1679,7 +1421,7 @@ mod tests { let mut topo = TopoGraph::new(); topo.set_topo(4, &"dummy".to_string(), &"".to_string(), &0, true, 1, false); let mut sched = - Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false, false); + Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false); sched.sched_circuit().expect("sched_circuit failed"); sched } @@ -1776,66 +1518,4 @@ mod tests { sched.t_gate_failures ); } - - #[test] - fn scheduler_completes_with_jit_correction_enabled() { - // Two independent T gates on separate qubits; any failures should be - // handled by JIT S correction without panicking. - let lines = &["+X___", "-_X__"]; - let sched = run_scheduler(lines, 42); - assert!(!sched.lcycle_scheduled.is_empty()); - } - - #[test] - fn z_basis_operator_unchanged_after_t_failure() { - // Z → Z under S conjugation; an M product on the same qubit keeps Z. - // The scheduler must complete without panicking. - let lines = &["+X___", "+Z___"]; - let sched = run_scheduler(lines, 42); - assert!(!sched.lcycle_scheduled.is_empty()); - } - - #[test] - fn double_t_failure_cancels_parity() { - // Two T gates on the same qubit whose failures cancel (S² = Z = identity on routing). - // Use enough products and seeds so at least one seed produces two failures. - let lines = &["+X___", "+X___", "+X___"]; - for seed in 0u32..20 { - // Just verify the scheduler never panics regardless of how many failures occur. - let sched = run_scheduler(lines, seed); - assert!(!sched.lcycle_scheduled.is_empty()); - } - } - - #[test] - fn jit_correction_patches_operator_after_upstream_t_failure() { - // With seed 0, gates 0 and 1 both fail on a 3-gate all-X chain: - // gate 0 fails → parity[0] = true - // gate 1 scheduled → JIT fires: X → Y for product 1 - // gate 1 fails → parity[0] toggles back to false (S² = identity) - // gate 2 scheduled → parity[0] = false, no correction; stays X - let lines = &["+X___", "+X___", "+X___"]; - let sched = run_scheduler(lines, 0); - - // Both failures must have actually occurred for this test to be meaningful. - assert_eq!(sched.t_gate_failures, 2, "expected exactly 2 T failures with seed 0"); - - // apply_jit_s_correction() was called exactly once per product. - assert!(sched.jit_corrected[0], "product 0 should be marked jit_corrected"); - assert!(sched.jit_corrected[1], "product 1 should be marked jit_corrected"); - assert!(sched.jit_corrected[2], "product 2 should be marked jit_corrected"); - - // Product 1: gate 0 failed before it was scheduled → X patched to Y. - assert_eq!( - sched.input.circuit.product(1).operators[0].basis, 'Y', - "product 1 operator should be X→Y after gate 0 failure" - ); - - // Product 2: gate 0 and gate 1 both failed before it was scheduled. - // Two S corrections cancel (S² = I), so the operator stays X. - assert_eq!( - sched.input.circuit.product(2).operators[0].basis, 'X', - "product 2 operator should remain X: double failure cancels" - ); - } } From 80cd96dd23d2285e54d022fa77ddf3eb5637a57a Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Fri, 24 Jul 2026 03:32:26 +0000 Subject: [PATCH 13/22] Add --record-cultivation-dist flag to control cultivation dist output The cultivation-time distribution file is now written only when -C / --record-cultivation-dist is passed; it is suppressed by default. --- src/puremagic.rs | 4 ++++ src/scheduler.rs | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/puremagic.rs b/src/puremagic.rs index 9675c6d..e20fa9b 100644 --- a/src/puremagic.rs +++ b/src/puremagic.rs @@ -67,6 +67,9 @@ struct Args { /// Disable T gate failures (every T gate succeeds on first attempt) #[arg(short = 'F', long)] no_t_failures: bool, + /// Record normalized cultivation-time distribution to .cultivation_dist + #[arg(short = 'C', long)] + record_cultivation_dist: bool, /// Number of ancilla between each data patch (all magic routing only) #[arg(short, long, default_value = "1")] ancilla_rows: usize, @@ -146,6 +149,7 @@ fn main() -> Result<(), Box> { args.plot.join(" "), args.rseed, args.no_t_failures, + args.record_cultivation_dist, ); let (tot_lcycles, n_scheduled) = sched.sched_circuit()?; diff --git a/src/scheduler.rs b/src/scheduler.rs index 7967f47..b29321c 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -189,6 +189,7 @@ pub(crate) struct Scheduler { pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, + record_cultivation_dist: bool, /// Reusable buffers to avoid per-lcycle allocations. terminals_buf: Vec, scheduled_ids_buf: Vec, @@ -214,7 +215,7 @@ pub(crate) struct Scheduler { impl Scheduler { pub(crate) fn new( circuit: Circuit, topo: TopoGraph, magic_state_lambda: f64, log_level: &str, - plot_option: String, rseed: u32, no_t_failures: bool, + plot_option: String, rseed: u32, no_t_failures: bool, record_cultivation_dist: bool, ) -> Self { if log_level != "none" { let trace_fname = format!("{}.sched_trace", circuit_stem(&circuit.circuit_fname)); @@ -249,6 +250,7 @@ impl Scheduler { stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, + record_cultivation_dist, terminals_buf: Vec::new(), scheduled_ids_buf: Vec::new(), children_buf: Vec::new(), @@ -1109,11 +1111,12 @@ impl Scheduler { println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); - // Write normalized cultivation-time distribution to file. - let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); - match self.write_cultivation_dist(&dist_fname, min, max) { - Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), - Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + if self.record_cultivation_dist { + let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); + match self.write_cultivation_dist(&dist_fname, min, max) { + Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), + Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + } } } @@ -1421,7 +1424,7 @@ mod tests { let mut topo = TopoGraph::new(); topo.set_topo(4, &"dummy".to_string(), &"".to_string(), &0, true, 1, false); let mut sched = - Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false); + Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false, false); sched.sched_circuit().expect("sched_circuit failed"); sched } From dd0a85f073758bdab40e96af9926fd736b94b513 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 20:25:12 +0000 Subject: [PATCH 14/22] Remove unused product_mut method --- src/circuit.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index 7b01de2..88cba1c 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -95,10 +95,6 @@ impl Circuit { &self.pps[id as usize] } - pub(crate) fn product_mut(&mut self, id: i32) -> &mut PauliProduct { - &mut self.pps[id as usize] - } - pub(crate) fn n_products(&self) -> usize { self.pps.len() } From 18996580bbffbf6d1dfe682d5999e290fa1373ca Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 23:16:58 +0000 Subject: [PATCH 15/22] Add lazy just-in-time T gate correction tracking When a T gate injection fails, instead of immediately inserting an S gate, track a per-qubit S^k correction power (0-3) and conjugate future T gates just-in-time (X<->Y swap for odd power). The correction is deferred until a single-qubit Clifford (S/SX) is encountered on that qubit, at which point a physical S gate is emitted before the Clifford proceeds. Key properties: - Consecutive T failures on the same qubit cancel in pairs (power mod 4) so no correction gate is needed when power reaches 0 or 2 (Z, classical) - The correction gate count is bounded by N_SX/2 where N_SX is the number of S/SX Cliffords in the circuit; measured ratios are 0.40-0.71 across 10 max-weight-1 benchmark circuits - CX gates are unaffected; corrections only apply to S/SX single-qubit Cliffords Also fixes: - Progress bar overflow when dynamically added correction gates increase scheduled_products.len() past tot_pps_to_sched - Plotting branch entered spuriously when plotting==false - sched_circuit returned inflated count (loaded + failures) instead of unique scheduled products - Deadlock: mark_blocked_product_as_used was called even when correction S gate emission failed, blocking the qubit node unnecessarily - Panic: schedule_lcycle panicked when pp_paths was empty and magic was available, but no T gates were actually pending (only deferred Cliffords) --- src/circuit.rs | 76 ++++++++++--- src/scheduler.rs | 290 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 330 insertions(+), 36 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index 88cba1c..a6bde3e 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,5 +1,6 @@ use crate::fn_timer; -use crate::pauliproduct::{GateType, PauliProduct}; +use crate::pauliproduct::{GateType, Operator, PauliProduct}; +use std::collections::HashMap; use plotters::coord::types::{RangedCoordf64, RangedCoordusize}; use plotters::prelude::*; use rand::Rng; @@ -386,10 +387,32 @@ impl Circuit { /// (mirroring [`Circuit::gen_deps`]); the original `self.pps` and `self.layers` cache /// are never modified. pub(crate) fn estimate_num_layers(&self, rng: &mut StdRng, no_t_failures: bool) -> usize { + // Per-qubit S^k correction power (0=none, 1=S, 2=Z, 3=S†). + // Incremented on each T gate failure; cleared when a Clifford is encountered. + let mut correction_power: HashMap = HashMap::new(); + // Build the expanded product list. let mut expanded: Vec = Vec::with_capacity(self.pps.len() * 4); for pp in &self.pps { if pp.gate_type.is_cx() { + // Before a CX, emit S corrections for any tracked qubits. + for op in &pp.operators { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power == 1 || power == 3 { + for _ in 0..3 { + let corr = PauliProduct { + id: expanded.len() as i32, + gate_type: GateType::S, + operators: vec![Operator { qubit: op.qubit, basis: op.basis }], + parents: vec![], + children: vec![], + max_qubit: op.qubit, + }; + expanded.push(corr); + } + } + correction_power.insert(op.qubit, 0); + } // CX → 2 sequential copies for _ in 0..2 { let mut copy = pp.clone(); @@ -399,6 +422,24 @@ impl Circuit { expanded.push(copy); } } else if pp.gate_type.is_s() || pp.gate_type.is_sx() { + // Before an S/SX, emit S correction for the tracked qubit if needed. + if let Some(op) = pp.operators.first() { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power == 1 || power == 3 { + for _ in 0..3 { + let corr = PauliProduct { + id: expanded.len() as i32, + gate_type: GateType::S, + operators: vec![Operator { qubit: op.qubit, basis: op.basis }], + parents: vec![], + children: vec![], + max_qubit: op.qubit, + }; + expanded.push(corr); + } + } + correction_power.insert(op.qubit, 0); + } // S / SX → 3 sequential copies for _ in 0..3 { let mut copy = pp.clone(); @@ -408,23 +449,28 @@ impl Circuit { expanded.push(copy); } } else if pp.gate_type.is_t() { - // T → 1 copy; if T failures are enabled, 50% chance of an S correction + // Apply S^k conjugation just-in-time: swap X↔Y for qubits with odd power. let mut t_copy = pp.clone(); t_copy.id = expanded.len() as i32; t_copy.parents.clear(); t_copy.children.clear(); - expanded.push(t_copy); + for op in &mut t_copy.operators { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power % 2 == 1 { + op.basis = + if op.basis == 'X' { 'Y' } else if op.basis == 'Y' { 'X' } else { op.basis }; + } + } + expanded.push(t_copy.clone()); if !no_t_failures && rng.gen_bool(0.5) { - // S correction uses the same qubits as the T gate - let mut s_copy = pp.clone(); - s_copy.id = expanded.len() as i32; - s_copy.gate_type = GateType::S; - s_copy.parents.clear(); - s_copy.children.clear(); - expanded.push(s_copy); + // T gate failed: increment correction power for each operator qubit. + for op in &t_copy.operators { + let entry = correction_power.entry(op.qubit).or_insert(0); + *entry = (*entry + 1) % 4; + } } } else { - // M and anything else → 1 copy + // M and anything else → 1 copy (no correction emission for measurements). let mut copy = pp.clone(); copy.id = expanded.len() as i32; copy.parents.clear(); @@ -1019,8 +1065,12 @@ mod tests { #[test] fn n_cycles_t_gate_s_insertion_varies_across_seeds() { - // With enough T gates, different seeds should produce different layer counts. - let lines: Vec<&str> = vec!["+X_"; 20]; + // With the lazy correction model, S corrections are emitted only when a Clifford + // gate is encountered. Use a chain of T gates followed by an S gate so that the + // accumulated correction is flushed before the S, producing different layer counts + // depending on how many T gates failed (odd failures → extra 3-layer correction). + let mut lines: Vec<&str> = vec!["+X_"; 19]; + lines.push("+X_"); let f = make_circuit_file(&lines); let mut c = Circuit::new(&f.path().to_string_lossy().to_string()); c.load_circuit().unwrap(); diff --git a/src/scheduler.rs b/src/scheduler.rs index b29321c..3e55c76 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -6,7 +6,7 @@ use crate::debug_sched; use crate::fn_timer; use crate::info_sched; use crate::node::NodeType; -use crate::pauliproduct::{Operator, PauliProduct}; +use crate::pauliproduct::{GateType, Operator, PauliProduct}; use crate::steinertree::SteinerTree; use crate::topograph::TopoGraph; use crate::treegraph::TreeGraph; @@ -210,6 +210,17 @@ pub(crate) struct Scheduler { current_lcycle: usize, /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). recovery_t_ids: Vec, + /// Per-qubit accumulated S^k correction power (0=none, 1=S, 2=Z, 3=S†). + /// Incremented on T gate failure; cleared when a Clifford is encountered or power cycles to 0. + correction_power: Vec, + /// Pauli basis of the most-recent conjugated T-gate operator per qubit. + /// Used as the operator basis when emitting an S correction before a Clifford. + correction_basis: Vec, + /// Next ID to assign when creating a dynamic S correction product. + next_correction_id: i32, + /// Maps product_id → (qubit, new_basis) pairs for T gates whose operators were + /// conjugated this lcycle. Populated in sched_remaining; consumed in process_t_gate_outcomes. + t_conjugated_bases: HashMap>, } impl Scheduler { @@ -217,6 +228,8 @@ impl Scheduler { circuit: Circuit, topo: TopoGraph, magic_state_lambda: f64, log_level: &str, plot_option: String, rseed: u32, no_t_failures: bool, record_cultivation_dist: bool, ) -> Self { + let n_qubits = circuit.n_qubits; + let n_initial_products = circuit.n_products() as i32; if log_level != "none" { let trace_fname = format!("{}.sched_trace", circuit_stem(&circuit.circuit_fname)); let level_filter = match log_level.to_lowercase().as_str() { @@ -266,6 +279,10 @@ impl Scheduler { pp_paths: Vec::new(), current_lcycle: 0, recovery_t_ids: Vec::new(), + correction_power: vec![0u8; n_qubits], + correction_basis: vec!['X'; n_qubits], + next_correction_id: n_initial_products, + t_conjugated_bases: HashMap::new(), } } @@ -330,22 +347,23 @@ impl Scheduler { // Show a progress bar during the non-plotting middle section of the run. // The first `plot_lcycles` and last `plot_lcycles` are plotted instead. if self.current_lcycle >= plot_lcycles - && (tot_pps_to_sched - n_scheduled >= plot_lcycles) + && n_scheduled + plot_lcycles <= tot_pps_to_sched { if self.current_lcycle == plot_lcycles { print!("Scheduling {} products: ", tot_pps_to_sched); } - let pct_complete = (n_scheduled * 100) / tot_pps_to_sched; + let pct_complete = + ((n_scheduled * 100) / tot_pps_to_sched).min(100); if pct_complete > prev_pct_complete { // \x08 is backspace; overwrite the previous "XX%" in-place. print!("\x08\x08\x08{:02}%", pct_complete); std::io::stdout().flush()?; prev_pct_complete = pct_complete; } - if tot_pps_to_sched - n_scheduled == plot_lcycles { + if n_scheduled + plot_lcycles == tot_pps_to_sched { print!("\n"); } - } else { + } else if plotting { let plot_info_str = self.stats.plot_info_str(); assert!(!plot_info_str.is_empty()); let fname_added = format!(".{}", self.current_lcycle); @@ -411,9 +429,7 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // Return total lcycles and total "attempts" (successes + failures), so the - // caller can compute the true T-gate count including failed first attempts. - Ok((self.current_lcycle, self.scheduled_products.len() + self.t_gate_failures)) + Ok((self.current_lcycle, self.scheduled_products.len())) } fn init_magic_nodes(&mut self) { @@ -543,6 +559,95 @@ impl Scheduler { println!("Precomputed {} multi-term Clifford trees", n_precomputed); } + /// Computes root candidate info for a set of already-computed terminal node IDs. + /// Extracted from `precompute_terminals_and_roots` so it can be called for dynamically + /// constructed operator lists (e.g. after S^k conjugation of T gate operators). + fn compute_root_info_for_terminals( + topo: &TopoGraph, terminals: &[u16], + ) -> Vec<(bool, Vec, Vec)> { + let mut root_info = Vec::with_capacity(terminals.len()); + for &term_id in terminals { + let node = topo.node(term_id); + let is_paired = + node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); + let mut preferred = Vec::new(); + let mut side = Vec::new(); + if is_paired { + let is_x = topo.label(term_id).contains('X'); + for &nb_id in node.nbs_slice() { + let nb = topo.node(nb_id); + if !nb.is_routing() { + continue; + } + if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { + preferred.push(nb_id); + } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + side.push(nb_id); + } + } + } else { + for &nb_id in node.nbs_slice() { + let nb = topo.node(nb_id); + if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + preferred.push(nb_id); + } + } + } + root_info.push((is_paired, preferred, side)); + } + root_info + } + + /// Applies the current per-qubit S^k conjugation to a T gate's operators just-in-time, + /// updates `precomputed_terminals` and `precomputed_root_info` for the product, and + /// returns the list of (qubit, new_basis) pairs where a swap occurred. + /// + /// Always recomputes from the circuit's original operators so that correction_power + /// changes between lcycles are always reflected correctly. + fn apply_t_conjugation(&mut self, pp_id: i32) -> Vec<(u16, char)> { + // Copy original operator data to release the borrow on self.input.circuit. + let orig_ops: Vec<(u16, char)> = self + .input + .circuit + .product(pp_id) + .operators + .iter() + .map(|op| (op.qubit, op.basis)) + .collect(); + // Apply S^k conjugation: odd power swaps X↔Y (Z unchanged). + let new_ops: Vec = orig_ops + .iter() + .map(|&(qubit, basis)| { + let new_basis = if self.correction_power[qubit as usize] % 2 == 1 { + if basis == 'X' { 'Y' } else if basis == 'Y' { 'X' } else { basis } + } else { + basis + }; + Operator { qubit, basis: new_basis } + }) + .collect(); + let swapped: Vec<(u16, char)> = orig_ops + .iter() + .zip(new_ops.iter()) + .filter(|&(&(_, ob), new_op)| ob != new_op.basis) + .map(|(_, new_op)| (new_op.qubit, new_op.basis)) + .collect(); + for &(q, new_basis) in &swapped { + info_sched!( + " T gate {}: qubit {} conjugated to {} (S^{} correction active)", + pp_id, + q, + new_basis, + self.correction_power[q as usize] + ); + } + let terminals = operators_to_node_ids(&self.input.topo, &new_ops); + let root_info = Self::compute_root_info_for_terminals(&self.input.topo, &terminals); + self.precomputed_terminals[pp_id as usize] = terminals; + self.precomputed_root_info[pp_id as usize] = root_info; + swapped + } + /// Precomputes terminal node IDs and root candidates for every product. /// /// For each terminal data node the root candidates are split into: @@ -664,20 +769,27 @@ impl Scheduler { ); if self.pp_paths.is_empty() { if n_avail_magic > 0 { - panic!( - "{}", - format!( - "lcycle {}: Cannot schedule products [{}] on current layout ({} magic)", - self.current_lcycle, - self.pps_pending - .iter() - .map(|pp| pp.to_operator_str()) - .collect::>() - .join(", "), - n_avail_magic, - ) - .red() - ); + // Only panic when T gates are pending but couldn't consume available magic. + // If all pending products are Cliffords whose correction S gate emission failed + // this lcycle (e.g. routing area temporarily congested), they will retry next + // lcycle — that is not a bug. + let has_pending_t = self.pps_pending.iter().any(|pp| pp.gate_type.is_t()); + if has_pending_t { + panic!( + "{}", + format!( + "lcycle {}: Cannot schedule products [{}] on current layout ({} magic)", + self.current_lcycle, + self.pps_pending + .iter() + .map(|pp| pp.to_operator_str()) + .collect::>() + .join(", "), + n_avail_magic, + ) + .red() + ); + } } false } else { @@ -743,10 +855,91 @@ impl Scheduler { } } + /// Attempts to emit a physical S correction gate for `qubit` when `correction_power[qubit]` + /// is non-zero. Returns `true` if the qubit's correction was handled (gate emitted or power=2 + /// classical-only), `false` if scheduling the physical S gate failed this lcycle. + /// + /// On success the correction power is cleared to 0. On failure (NoPath), the circuit push is + /// rolled back and the correction power is left unchanged so the caller can retry next lcycle. + fn try_emit_s_correction(&mut self, qubit: u16, plotting: bool) -> bool { + let q = qubit as usize; + let power = self.correction_power[q]; + info_sched!( + " Correction emit for qubit {} (power={}, basis={})", + qubit, + power, + self.correction_basis[q] + ); + if power == 0 { + return true; + } + if power == 2 { + // Z correction: tracked classically — no physical gate needed. + info_sched!(" Qubit {}: Z correction (classical), clearing power", qubit); + self.correction_power[q] = 0; + return true; + } + // power 1 (S) or 3 (S†): emit a physical S gate on the qubit. + let s_id = self.next_correction_id; + let basis = self.correction_basis[q]; + let s_pp = PauliProduct { + id: s_id, + gate_type: GateType::S, + operators: vec![Operator { qubit, basis }], + parents: vec![], + children: vec![], + max_qubit: qubit, + }; + self.input.circuit.pps.push(s_pp); + let new_ops = vec![Operator { qubit, basis }]; + let terminals = operators_to_node_ids(&self.input.topo, &new_ops); + let root_info = Self::compute_root_info_for_terminals(&self.input.topo, &terminals); + self.precomputed_terminals.push(terminals); + self.precomputed_root_info.push(root_info); + + if !self.terminal_nodes(s_id) { + self.input.circuit.pps.pop(); + self.precomputed_terminals.pop(); + self.precomputed_root_info.pop(); + return false; + } + let result = self.sched_s_sx(s_id, plotting); + match result { + PathResult::PathFound(opt_graph) => { + info_sched!( + " Emitted correction S gate id={} for qubit {} (power={}), clearing", + s_id, + qubit, + power + ); + self.next_correction_id += 1; + if let Some(ref g) = opt_graph { + let node_ids: Vec = g.iter_nodes().collect(); + self.mark_nodes_used(&node_ids); + } + self.pp_paths.push((s_id, opt_graph.map(Rc::new))); + self.correction_power[q] = 0; + true + } + PathResult::NoPath => { + info_sched!( + " Correction S gate emit for qubit {} (power={}) failed (NoPath), will retry next lcycle", + qubit, + power + ); + self.input.circuit.pps.pop(); + self.precomputed_terminals.pop(); + self.precomputed_root_info.pop(); + false + } + } + } + /// Second pass: greedily schedule T gates, measurements, and S/SX gates. /// T gates are skipped when no magic state is available. fn sched_remaining(&mut self, n_avail_magic: &mut usize, plotting: bool) { let _timer = accum_start!(self.timers); + self.t_conjugated_bases.clear(); for i in 0..self.pps_pending.len() { let pp_id = self.pps_pending[i].id; let pp = self.input.circuit.product(pp_id); @@ -754,6 +947,35 @@ impl Scheduler { continue; } let (pp_id, gate_type) = (pp.id, pp.gate_type); + // For single-qubit Clifford gates (S/SX), check for pending correction and emit + // a physical S gate before proceeding. CX is multi-qubit and handled by + // sched_precomputed, so it never appears here. + if gate_type.is_s() || gate_type.is_sx() { + let op_qubit = self.input.circuit.product(pp_id).operators[0].qubit; + if self.correction_power[op_qubit as usize] != 0 { + info_sched!( + " Clifford {:?} {} (qubit {}) deferred: pending correction power={}", + gate_type, + pp_id, + op_qubit, + self.correction_power[op_qubit as usize] + ); + let emitted = self.try_emit_s_correction(op_qubit, plotting); + // Block the Clifford's qubit node only when the correction S gate was + // successfully placed; if emission failed, leave the node free so T gates + // can still route through it this lcycle (avoids deadlock). + if emitted { + let pp = self.input.circuit.product(pp_id); + Self::mark_blocked_product_as_used(&mut self.used, &self.input.topo, pp); + } + continue; + } + } + // For T gates, apply just-in-time S^k conjugation (X↔Y swap for odd power). + if gate_type.is_t() { + let swapped = self.apply_t_conjugation(pp_id); + self.t_conjugated_bases.insert(pp_id, swapped); + } if *n_avail_magic > 0 || !gate_type.is_t() { info_sched!(" Trying to schedule product {}", self.input.circuit.product(pp_id)); let result = if !self.terminal_nodes(pp_id) { @@ -983,6 +1205,28 @@ impl Scheduler { " T gate {} failed (50% probability), recovery lcycle next", pp_id ); + // Update per-qubit correction tracking using the conjugated operator bases + // recorded in sched_remaining. Each failed qubit increments its S^k power. + let pp = self.input.circuit.product(pp_id); + let conjugated_map: HashMap = self + .t_conjugated_bases + .get(&pp_id) + .map(|v| v.iter().copied().collect()) + .unwrap_or_default(); + let orig_ops: Vec<(u16, char)> = + pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); + for (qubit, orig_basis) in orig_ops { + let q = qubit as usize; + self.correction_power[q] = (self.correction_power[q] + 1) % 4; + self.correction_basis[q] = + conjugated_map.get(&qubit).copied().unwrap_or(orig_basis); + info_sched!( + " Qubit {} correction: power now {} (basis={})", + qubit, + self.correction_power[q], + self.correction_basis[q] + ); + } } } } @@ -1160,7 +1404,7 @@ impl Scheduler { )); } } - for nid in operators_to_node_ids(&self.input.topo, &pp.operators) { + for &nid in &self.precomputed_terminals[pp_id as usize] { if !tree.contains_node(nid) { return Err(io::Error::new( io::ErrorKind::Other, From 897c461802166734788abc155312161bb7009777 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 23:19:39 +0000 Subject: [PATCH 16/22] Fix release build warnings: guard logging-only loop with #[cfg(debug_assertions)] --- src/scheduler.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scheduler.rs b/src/scheduler.rs index 3e55c76..7c163b5 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -632,6 +632,7 @@ impl Scheduler { .filter(|&(&(_, ob), new_op)| ob != new_op.basis) .map(|(_, new_op)| (new_op.qubit, new_op.basis)) .collect(); + #[cfg(debug_assertions)] for &(q, new_basis) in &swapped { info_sched!( " T gate {}: qubit {} conjugated to {} (S^{} correction active)", From 1492793e27e925fe4d12702d9d8ea84b1ba9bf35 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sat, 25 Jul 2026 23:33:42 +0000 Subject: [PATCH 17/22] Print S correction gate count and percentage at end of run --- src/puremagic.rs | 10 ++++++++++ src/scheduler.rs | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src/puremagic.rs b/src/puremagic.rs index e20fa9b..94dcc77 100644 --- a/src/puremagic.rs +++ b/src/puremagic.rs @@ -111,6 +111,7 @@ fn main() -> Result<(), Box> { let mut circuit = Circuit::new(&circuit_fname); circuit.load_circuit()?; let n_products = circuit.n_products(); + let n_sx_cliffords = circuit.pps.iter().filter(|pp| pp.gate_type.is_s() || pp.gate_type.is_sx()).count(); let _n_layers = circuit.print_statistics(); #[cfg(debug_assertions)] circuit.print()?; @@ -156,6 +157,15 @@ fn main() -> Result<(), Box> { assert!(n_scheduled >= n_products); let volume = n_qubits * tot_lcycles; println!("Scheduled {} in {} logical cycles, volume {}", n_scheduled, tot_lcycles, volume); + if !args.no_t_failures && n_sx_cliffords > 0 { + let corrections = sched.correction_gates_emitted; + println!( + "S correction gates: {} / {} S/SX Cliffords ({:.1}%)", + corrections, + n_sx_cliffords, + corrections as f64 * 100.0 / n_sx_cliffords as f64 + ); + } println!("Parallelism: {:.3}x", n_scheduled as f64 / tot_lcycles as f64); //let min_layers = if args.no_t_failures { n_layers } else { (n_layers * 3) / 2 }; diff --git a/src/scheduler.rs b/src/scheduler.rs index 7c163b5..9cfcb36 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -186,6 +186,7 @@ pub(crate) struct Scheduler { /// T gates that failed the 50% coin flip; held for one recovery lcycle. failed_t_paths: IndexMap, Option>)>, pub(crate) t_gate_failures: usize, + pub(crate) correction_gates_emitted: usize, pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, @@ -260,6 +261,7 @@ impl Scheduler { clifford_paths: IndexMap::new(), failed_t_paths: IndexMap::new(), t_gate_failures: 0, + correction_gates_emitted: 0, stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, @@ -914,6 +916,7 @@ impl Scheduler { power ); self.next_correction_id += 1; + self.correction_gates_emitted += 1; if let Some(ref g) = opt_graph { let node_ids: Vec = g.iter_nodes().collect(); self.mark_nodes_used(&node_ids); From 60e27874647547208eb3f42b42ef2da9975cb73a Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sun, 26 Jul 2026 00:23:47 +0000 Subject: [PATCH 18/22] Fix scheduling failure at high magic production rates (-m 100) Two bugs caused "Cannot schedule on current layout" errors when magic states are produced faster than they are consumed: 1. terminal_nodes vacuous-truth false-negative: when a correction S gate's terminal has no same-row routing neighbor in its precomputed root_info (because sched_s_sx uses any same-row neighbor, not just routing nodes), both preferred and side lists are empty. iter().all() over an empty iterator is vacuously true, so the "all roots occupied" guard fired unconditionally, making try_emit_s_correction always fail even when all nodes were free. Fixed by guarding the check with (!preferred.is_empty() || !side.is_empty()). 2. Outer-loop cultivation check too strict: with very high -m, magic is produced instantly so is_cultivating is always false. When a correction S gate emission fails and only Cliffords remain in pps_pending, the scheduler returned an error instead of continuing. Fixed by only checking cultivation when T gates are pending (T gates need magic; Cliffords don't). --- src/scheduler.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 9cfcb36..674f0ff 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -413,10 +413,14 @@ impl Scheduler { } } else { debug_sched!("Could not schedule anything on lcycle {}", self.current_lcycle); - // If nothing is cultivating, no magic state will ever become ready, - // so the layout is fundamentally unable to make progress. - if !(0..self.input.topo.n_nodes) - .any(|node_i| self.input.topo.is_cultivating(node_i as u16)) + // If nothing is cultivating, no magic state will ever become ready — + // but only T gates need magic. If all pending products are Cliffords + // (e.g. correction S gate emission temporarily failed), they don't need + // magic and will route once the area frees up; don't error in that case. + let has_pending_t = self.pps_pending.iter().any(|pp| pp.gate_type.is_t()); + if has_pending_t + && !(0..self.input.topo.n_nodes) + .any(|node_i| self.input.topo.is_cultivating(node_i as u16)) { return Err(io::Error::new( io::ErrorKind::Other, @@ -477,7 +481,11 @@ impl Scheduler { return false; } let (_, preferred, side) = &root_info[i]; - if preferred.iter().all(|&rid| self.used[rid as usize]) + // Only reject when there are root candidates and all are occupied. + // Empty preferred+side means the gate uses sched_s_sx (no root needed), + // so the vacuous-truth case must not fire here. + if (!preferred.is_empty() || !side.is_empty()) + && preferred.iter().all(|&rid| self.used[rid as usize]) && side.iter().all(|&rid| self.used[rid as usize]) { info_sched!(" No unused root candidates for node {}", node_id); From a06934324bc8aca90b3a3a5d4611a28a60f2b95a Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sun, 26 Jul 2026 01:29:47 +0000 Subject: [PATCH 19/22] Remove recovery lcycle mechanism; T gate failures are purely classical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The failed_t_paths recovery mechanism was the old way of physically applying S corrections: when a T gate failed, a second routing lcycle reused the trimmed path to complete the correction. With lazy correction tracking, this is unnecessary — the S correction lives in correction_power and is only emitted as a physical gate when a Clifford is encountered. Remove failed_t_paths, recovery_t_ids, and all carry-forward logic for recovery T gates. T gate failures now complete in exactly 1 lcycle (same as successes), with correction_power incremented classically. Children are unlocked immediately after the lcycle. Result: max-weight-0 circuits (no Cliffords) show ~1.000x lcycle ratio with vs. without failures. Max-weight-1 circuits show ~1.000x lcycles with a small number of extra S correction gates at Clifford boundaries. --- src/scheduler.rs | 180 +++++++++++++---------------------------------- src/treegraph.rs | 1 + 2 files changed, 51 insertions(+), 130 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 674f0ff..fec81a1 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -183,8 +183,6 @@ pub(crate) struct Scheduler { /// (remaining_lcycles, product, node_ids, opt_tree). /// CX occupies 2 lcycles; S/SX occupy 3. clifford_paths: IndexMap, Option>)>, - /// T gates that failed the 50% coin flip; held for one recovery lcycle. - failed_t_paths: IndexMap, Option>)>, pub(crate) t_gate_failures: usize, pub(crate) correction_gates_emitted: usize, pub(crate) stree_computation: SteinerTree, @@ -209,8 +207,6 @@ pub(crate) struct Scheduler { remaining_parents: Vec, pub(crate) pp_paths: Vec<(i32, Option>)>, current_lcycle: usize, - /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). - recovery_t_ids: Vec, /// Per-qubit accumulated S^k correction power (0=none, 1=S, 2=Z, 3=S†). /// Incremented on T gate failure; cleared when a Clifford is encountered or power cycles to 0. correction_power: Vec, @@ -259,7 +255,6 @@ impl Scheduler { scheduled_products: IndexSet::new(), used: vec![false; n_nodes], clifford_paths: IndexMap::new(), - failed_t_paths: IndexMap::new(), t_gate_failures: 0, correction_gates_emitted: 0, stree_computation: SteinerTree::new(n_nodes), @@ -280,7 +275,6 @@ impl Scheduler { remaining_parents: Vec::new(), pp_paths: Vec::new(), current_lcycle: 0, - recovery_t_ids: Vec::new(), correction_power: vec![0u8; n_qubits], correction_basis: vec!['X'; n_qubits], next_correction_id: n_initial_products, @@ -327,7 +321,6 @@ impl Scheduler { self.pp_paths = Vec::new(); while !self.pps_pending.is_empty() || !self.clifford_paths.is_empty() - || !self.failed_t_paths.is_empty() { self.timers.start(self.loop_timer); self.current_lcycle += 1; @@ -397,7 +390,7 @@ impl Scheduler { } } } else if pp.gate_type.is_t() { - if self.recovery_t_ids.contains(pp_id) { 2 } else { 1 } + 1 } else { 1 }; @@ -743,7 +736,7 @@ impl Scheduler { let initial_magic = n_avail_magic; self.pp_paths.clear(); self.used.fill(false); - // Collect carry-forward data before the loop to release borrows on the maps. + // Collect carry-forward data before the loop to release borrows on the map. let clifford_carry: Vec<(i32, Vec, Option>)> = self .clifford_paths .values() @@ -751,20 +744,10 @@ impl Scheduler { (pp.id, node_ids.clone(), opt_tree.as_ref().map(Rc::clone)) }) .collect(); - let failed_t_carry: Vec<(i32, Vec, Option>)> = self - .failed_t_paths - .values() - .map(|(pp, node_ids, opt_tree)| { - (pp.id, node_ids.clone(), opt_tree.as_ref().map(Rc::clone)) - }) - .collect(); for (pp_id, node_ids, opt_tree) in clifford_carry { self.carry_forward_path(pp_id, &node_ids, opt_tree); } - for (pp_id, node_ids, opt_tree) in failed_t_carry { - self.carry_forward_path(pp_id, &node_ids, opt_tree); - } - let carry_forward_count = self.clifford_paths.len() + self.failed_t_paths.len(); + let carry_forward_count = self.clifford_paths.len(); let tot_available = carry_forward_count + self.pps_pending.len(); info_sched!(" Remaining to schedule: {}", self.pps_pending.len()); self.sched_precomputed(plotting); @@ -1149,21 +1132,15 @@ impl Scheduler { self.scheduled_ids_buf.extend(self.pp_paths.iter().map(|(id, _)| *id)); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); - // Only count T gates that are newly scheduled (not recovery lcycles) to - // keep the pool-size estimate accurate. let t_newly_scheduled = self .pp_paths .iter() - .filter(|(id, _)| { - self.input.circuit.product(*id).gate_type.is_t() - && !self.failed_t_paths.contains_key(id) - }) + .filter(|(id, _)| self.input.circuit.product(*id).gate_type.is_t()) .count(); self.cultivation.t_products_remaining = self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); - let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); - self.recovery_t_ids = t_recovery_ids; - self.unlock_children(&t_failed_ids); + self.process_t_gate_outcomes(); + self.unlock_children(); self.advance_clifford_state(); debug_sched!( "After inserting previous lcycle cliffords, pps_to_sched len {}", @@ -1176,105 +1153,60 @@ impl Scheduler { self.children_buf.len(), self.pps_pending.len() ); - let lcycle_ids: Vec = self - .pp_paths - .iter() - .filter(|(id, _)| !t_failed_ids.contains(id)) - .map(|(id, _)| *id) - .collect(); + let lcycle_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); self.lcycle_scheduled.push((self.current_lcycle, lcycle_ids)); #[cfg(debug_assertions)] - self.check_lcycle(&t_failed_ids, &self.recovery_t_ids)?; - self.scheduled_products.extend( - self.pp_paths.iter().filter(|(id, _)| !t_failed_ids.contains(id)).map(|(id, _)| *id), - ); + self.check_lcycle()?; + self.scheduled_products.extend(self.pp_paths.iter().map(|(id, _)| *id)); Ok(()) } - /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids). + /// Coin-flip T gate outcomes and update per-qubit correction tracking. /// - /// First-attempt T gates succeed with 50% probability (or always if `no_t_failures`). - /// Recovery-lcycle T gates (already in `failed_t_paths`) always succeed. - /// Failed gates are stored in `failed_t_paths` with the magic root trimmed off - /// so the routing subtree can be reused in the recovery lcycle. - fn process_t_gate_outcomes(&mut self) -> (Vec, Vec) { - let mut t_failed_ids: Vec = Vec::new(); - let mut t_recovery_ids: Vec = Vec::new(); + /// T gates complete in one lcycle regardless of outcome. A failed T gate + /// increments `correction_power` for each of its qubits (tracked classically); + /// no recovery lcycle is needed. + fn process_t_gate_outcomes(&mut self) { let pp_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); - for pp_id in &pp_ids { - let pp_id = *pp_id; - let pp = self.input.circuit.product(pp_id); - if pp.gate_type.is_t() { - if self.failed_t_paths.contains_key(&pp_id) { - t_recovery_ids.push(pp_id); - info_sched!(" T gate {} recovery lcycle succeeded", pp_id); - } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { - info_sched!(" T gate {} succeeded on first attempt", pp_id); - } else { - t_failed_ids.push(pp_id); - self.t_gate_failures += 1; - info_sched!( - " T gate {} failed (50% probability), recovery lcycle next", - pp_id - ); - // Update per-qubit correction tracking using the conjugated operator bases - // recorded in sched_remaining. Each failed qubit increments its S^k power. - let pp = self.input.circuit.product(pp_id); - let conjugated_map: HashMap = self - .t_conjugated_bases - .get(&pp_id) - .map(|v| v.iter().copied().collect()) - .unwrap_or_default(); - let orig_ops: Vec<(u16, char)> = - pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); - for (qubit, orig_basis) in orig_ops { - let q = qubit as usize; - self.correction_power[q] = (self.correction_power[q] + 1) % 4; - self.correction_basis[q] = - conjugated_map.get(&qubit).copied().unwrap_or(orig_basis); - info_sched!( - " Qubit {} correction: power now {} (basis={})", - qubit, - self.correction_power[q], - self.correction_basis[q] - ); - } - } - } - } - let pp_paths_snapshot: Vec<(i32, Option>)> = - self.pp_paths.iter().map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))).collect(); - for (pp_id, opt_pp_path) in &pp_paths_snapshot { - let pp_id = *pp_id; + for pp_id in pp_ids { let pp = self.input.circuit.product(pp_id); if !pp.gate_type.is_t() { continue; } - if t_failed_ids.contains(&pp_id) { - // Store the routing subtree without the magic root so the recovery - // lcycle can reuse the same data/routing nodes while finding a new - // magic state. When not plotting, fall back to just the terminal IDs. - let trimmed_opt_tree: Option> = opt_pp_path.as_ref().map(|tree| { - let mut t = (**tree).clone(); - t.trim_magic_root(); - Rc::new(t) - }); - let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { - trimmed.iter_nodes().collect() - } else { - self.precomputed_terminals[pp_id as usize].clone() - }; - self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); + if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { + info_sched!(" T gate {} succeeded", pp_id); } else { - self.failed_t_paths.swap_remove(&pp_id); + self.t_gate_failures += 1; + info_sched!(" T gate {} failed (50% probability), S correction tracked", pp_id); + // Update per-qubit correction tracking using the conjugated operator bases + // recorded in sched_remaining. Each failed qubit increments its S^k power. + let pp = self.input.circuit.product(pp_id); + let conjugated_map: HashMap = self + .t_conjugated_bases + .get(&pp_id) + .map(|v| v.iter().copied().collect()) + .unwrap_or_default(); + let orig_ops: Vec<(u16, char)> = + pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); + for (qubit, orig_basis) in orig_ops { + let q = qubit as usize; + self.correction_power[q] = (self.correction_power[q] + 1) % 4; + self.correction_basis[q] = + conjugated_map.get(&qubit).copied().unwrap_or(orig_basis); + info_sched!( + " Qubit {} correction: power now {} (basis={})", + qubit, + self.correction_power[q], + self.correction_basis[q] + ); + } } } - (t_failed_ids, t_recovery_ids) } /// Decrements `remaining_parents` for each completed product and collects /// newly-ready children into `children_buf`. - fn unlock_children(&mut self, t_failed_ids: &[i32]) { + fn unlock_children(&mut self) { self.children_buf.clear(); for i in 0..self.pp_paths.len() { let pp_id = self.pp_paths[i].0; @@ -1294,9 +1226,6 @@ impl Scheduler { _ => {} } } - if gate_type.is_t() && t_failed_ids.contains(&pp_id) { - continue; - } let children: Vec = self.input.circuit.product(pp_id).children.clone(); for child_id in children { self.remaining_parents[child_id as usize] -= 1; @@ -1396,7 +1325,7 @@ impl Scheduler { } #[cfg(debug_assertions)] - fn check_lcycle(&self, _t_failed_ids: &[i32], t_recovery_ids: &[i32]) -> io::Result<()> { + fn check_lcycle(&self) -> io::Result<()> { let mut lcycle_used = vec![false; self.input.topo.n_nodes]; for &(pp_id, ref opt_tree) in &self.pp_paths { let Some(tree) = opt_tree else { continue }; @@ -1428,7 +1357,7 @@ impl Scheduler { )); } } - if pp.gate_type.is_t() && !t_recovery_ids.contains(&pp_id) { + if pp.gate_type.is_t() { match tree.root_node_id { None => { return Err(io::Error::new( @@ -1753,28 +1682,19 @@ mod tests { } #[test] - fn failed_t_paths_empty_after_schedule_completes() { - let lines = &["+X___", "-_X__", "+__X_", "-___X"]; - let sched = run_scheduler(lines, 0); - assert!( - sched.failed_t_paths.is_empty(), - "failed_t_paths not empty after schedule_circuit: {:?}", - sched.failed_t_paths.keys().collect::>() - ); - } - - #[test] - fn lcycle_count_bounded_by_t_gate_failure_overhead() { + fn lcycle_count_not_inflated_by_t_gate_failures() { + // T gate failures are tracked classically (correction_power); they do NOT + // add extra scheduling lcycles. All N independent T gates complete in at + // most N lcycles regardless of failure count. let lines = &["+X___", "-_X__", "+__X_", "-___X"]; let sched = run_scheduler(lines, 5); let n_t = 4usize; let active_lcycles = sched.lcycle_scheduled.len(); assert!( - active_lcycles <= n_t + sched.t_gate_failures, - "active lcycles {} > n_t {} + failures {}", + active_lcycles <= n_t, + "active lcycles {} > n_t {} (failures should not add extra lcycles)", active_lcycles, n_t, - sched.t_gate_failures ); } } diff --git a/src/treegraph.rs b/src/treegraph.rs index a45c078..eba974b 100644 --- a/src/treegraph.rs +++ b/src/treegraph.rs @@ -107,6 +107,7 @@ impl TreeGraph { /// Removes the magic root and trims dangling routing nodes. /// Routing nodes whose sole remaining nb is a data node are preserved. + #[cfg(test)] pub(crate) fn trim_magic_root(&mut self) { let root_id = match self.root_node_id.take() { Some(id) => id, From ea97e30075ded5982ecaae299249b02d2f73752e Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sun, 26 Jul 2026 01:58:35 +0000 Subject: [PATCH 20/22] Fix area fit line using wrong series in slash-y mode When plotting lcycles/ancilla_qubits with ratio file pairs, both series are ratio series on the same axis. The ancilla_qubits fit was picking ratio_series[0] (the lcycles/Time series) instead of the ancilla_qubits series, placing the fit line at the wrong height. Fix by tagging each Series with its y_key and filtering to the ancilla_qubits series when selecting data for the fit. --- data_processing/plot_puremagic.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/data_processing/plot_puremagic.py b/data_processing/plot_puremagic.py index 83836dd..91f6ab5 100755 --- a/data_processing/plot_puremagic.py +++ b/data_processing/plot_puremagic.py @@ -89,6 +89,7 @@ class Series: is_ratio: bool = False ratio_label: Optional[str] = None point_labels: Optional[list] = None + y_key: Optional[str] = None # --------------------------------------------------------------------------- @@ -903,6 +904,7 @@ def load_series(y_keys_for_axis, label_suffix=None): circuits=merged["circuit"].tolist(), is_ratio=True, ratio_label=ratio_label, + y_key=y_key, ) ) else: @@ -933,6 +935,7 @@ def load_series(y_keys_for_axis, label_suffix=None): ys=ys_vals, circuits=d1["circuit"].fillna("").tolist(), point_labels=pt_labels, + y_key=y_key, ) ) @@ -1079,8 +1082,10 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): ratio_series = [s for s in series_list if s.is_ratio] non_ratio_series = [s for s in series_list if not s.is_ratio] if ratio_series: - # Use the first ratio series (there is typically only one per axis) - s = ratio_series[0] + # In slash mode multiple ratio series share the axis; pick the one for + # ancilla_qubits, falling back to the first if none is tagged. + ancilla_rs = [s for s in ratio_series if s.y_key == "ancilla_qubits"] + s = ancilla_rs[0] if ancilla_rs else ratio_series[0] rx = np.array(s.xs, float) ry = np.array(s.ys, float) elif len(non_ratio_series) >= 2: @@ -1105,7 +1110,7 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): # Least-squares fit for y = A / sqrt(x): # minimise sum((y - A/sqrt(x))^2) => A = sum(y/sqrt(x)) / sum(1/x) A = np.sum(sy / np.sqrt(sx)) / np.sum(1.0 / sx) - # A *= 0.48 + # A *= 0.85 y_pred = A / np.sqrt(sx) ss_res = np.sum((sy - y_pred) ** 2) ss_tot = np.sum((sy - sy.mean()) ** 2) From 49a60e5ff211fcba04712b6f19217f099b8d2a74 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sun, 26 Jul 2026 04:05:24 +0000 Subject: [PATCH 21/22] Fix FLASQ x-axis using Q instead of circuit qubit count For circuits like QFT, Max simultaneous qubit usage (Q) is less than the actual number of data qubits (e.g. Q=14 for a 20-qubit QFT), causing FLASQ points to be plotted at the wrong x positions when using -x data_qubits. Switch to parsing Circuit qubits (n_qubits) which always matches the data_qubits values from the BUS/PureMagic output files. --- data_processing/plot_puremagic.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data_processing/plot_puremagic.py b/data_processing/plot_puremagic.py index 91f6ab5..e57e4fe 100755 --- a/data_processing/plot_puremagic.py +++ b/data_processing/plot_puremagic.py @@ -476,18 +476,18 @@ def parse_flasq_file(filepath): FLASQ Lower Bound for Layout: ... ======================================================================== - Max simultaneous qubit usage (Q) : + Circuit qubits (n_qubits) : ... FLASQ spacetime volume (S, blocks) : ======================================================================== The circuit name is taken from the "FLASQ Lower Bound for" header line, - Q from the "Max simultaneous qubit usage" row, and the two volumes from - the "FLASQ spacetime volume" row. + n_data_qubits from the "Circuit qubits (n_qubits)" row, and the two volumes + from the "FLASQ spacetime volume" row. """ entries = [] current_circuit = None - current_q = None + current_n_qubits = None with open(filepath) as f: for line in f: s = line.strip() @@ -495,12 +495,12 @@ def parse_flasq_file(filepath): m = re.match(r"^FLASQ Lower Bound for\s+(.+)$", s) if m: current_circuit = m.group(1).strip() - current_q = None + current_n_qubits = None continue - # Q row: "Max simultaneous qubit usage (Q) : " - m = re.match(r"Max simultaneous qubit usage \(Q\)\s*:\s*(\d+)", s) + # n_qubits row: "Circuit qubits (n_qubits) : " + m = re.match(r"Circuit qubits \(n_qubits\)\s*:\s*(\d+)", s) if m and current_circuit is not None: - current_q = int(m.group(1)) + current_n_qubits = int(m.group(1)) continue # Volume row: "FLASQ spacetime volume (S, blocks) : " m = re.match( @@ -511,11 +511,11 @@ def parse_flasq_file(filepath): try: vol_cons = float(m.group(1)) vol_opt = float(m.group(2)) - entries.append((current_circuit, vol_cons, vol_opt, current_q)) + entries.append((current_circuit, vol_cons, vol_opt, current_n_qubits)) except ValueError: pass current_circuit = None - current_q = None + current_n_qubits = None return entries From b123939b1fd065a8c3ebe39ecd367617e3409032 Mon Sep 17 00:00:00 2001 From: Steven Hofmeyr Date: Sun, 26 Jul 2026 04:36:31 +0000 Subject: [PATCH 22/22] Skip T conjugation when no correction is active apply_t_conjugation was called unconditionally for every pending T gate every lcycle, allocating multiple Vecs and recomputing terminals/root info even when correction_power was all zeros. For max-weight-1 circuits with many T gates across many lcycles this added ~40% overhead. Add conjugated_t_products to track which products have stale precomputed terminals, and skip apply_t_conjugation entirely unless an odd correction power is active or a prior conjugation needs restoring. Also simplify the conjugation to a single pass, eliminating one Vec allocation. Co-Authored-By: Claude Sonnet 4.6 --- src/scheduler.rs | 57 ++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index fec81a1..7acd3dd 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -17,7 +17,7 @@ use indexmap::{IndexMap, IndexSet}; use rand::Rng; use rand::SeedableRng; use rand::rngs::StdRng; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{self, BufWriter, Write}; use std::path::Path; @@ -218,6 +218,10 @@ pub(crate) struct Scheduler { /// Maps product_id → (qubit, new_basis) pairs for T gates whose operators were /// conjugated this lcycle. Populated in sched_remaining; consumed in process_t_gate_outcomes. t_conjugated_bases: HashMap>, + /// Set of T gate product IDs whose precomputed_terminals/root_info currently reflect a + /// conjugated (non-original) operator basis. Needed to trigger a restore call to + /// apply_t_conjugation when correction_power later drops back to zero. + conjugated_t_products: HashSet, } impl Scheduler { @@ -279,6 +283,7 @@ impl Scheduler { correction_basis: vec!['X'; n_qubits], next_correction_id: n_initial_products, t_conjugated_bases: HashMap::new(), + conjugated_t_products: HashSet::new(), } } @@ -617,24 +622,20 @@ impl Scheduler { .iter() .map(|op| (op.qubit, op.basis)) .collect(); - // Apply S^k conjugation: odd power swaps X↔Y (Z unchanged). - let new_ops: Vec = orig_ops - .iter() - .map(|&(qubit, basis)| { - let new_basis = if self.correction_power[qubit as usize] % 2 == 1 { - if basis == 'X' { 'Y' } else if basis == 'Y' { 'X' } else { basis } - } else { - basis - }; - Operator { qubit, basis: new_basis } - }) - .collect(); - let swapped: Vec<(u16, char)> = orig_ops - .iter() - .zip(new_ops.iter()) - .filter(|&(&(_, ob), new_op)| ob != new_op.basis) - .map(|(_, new_op)| (new_op.qubit, new_op.basis)) - .collect(); + // Apply S^k conjugation in a single pass: odd power swaps X↔Y (Z unchanged). + let mut new_ops = Vec::with_capacity(orig_ops.len()); + let mut swapped = Vec::new(); + for &(qubit, basis) in &orig_ops { + let new_basis = if self.correction_power[qubit as usize] % 2 == 1 { + if basis == 'X' { 'Y' } else if basis == 'Y' { 'X' } else { basis } + } else { + basis + }; + if new_basis != basis { + swapped.push((qubit, new_basis)); + } + new_ops.push(Operator { qubit, basis: new_basis }); + } #[cfg(debug_assertions)] for &(q, new_basis) in &swapped { info_sched!( @@ -967,9 +968,23 @@ impl Scheduler { } } // For T gates, apply just-in-time S^k conjugation (X↔Y swap for odd power). + // Skip entirely when no qubit in the product has an odd correction power AND + // the precomputed terminals are not stale from a prior conjugation — this is + // the common case and avoids Vec allocations + topology lookups every lcycle. if gate_type.is_t() { - let swapped = self.apply_t_conjugation(pp_id); - self.t_conjugated_bases.insert(pp_id, swapped); + let any_odd_correction = self.input.circuit.product(pp_id).operators + .iter() + .any(|op| self.correction_power[op.qubit as usize] % 2 == 1); + let was_conjugated = self.conjugated_t_products.contains(&pp_id); + if any_odd_correction || was_conjugated { + let swapped = self.apply_t_conjugation(pp_id); + if swapped.is_empty() { + self.conjugated_t_products.remove(&pp_id); + } else { + self.conjugated_t_products.insert(pp_id); + self.t_conjugated_bases.insert(pp_id, swapped); + } + } } if *n_avail_magic > 0 || !gate_type.is_t() { info_sched!(" Trying to schedule product {}", self.input.circuit.product(pp_id));