From 6c7128c50a51dfe7095cc5388cc18ce574257f2d Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 16 Jun 2026 16:14:13 -0700 Subject: [PATCH 1/4] make QRE deterministic --- source/qdk_package/qdk/qre/_qre.pyi | 8 +- source/qdk_package/qdk/qre/interop/_cirq.py | 20 ++-- source/qdk_package/src/qre.rs | 8 +- .../qdk_package/tests/qre/test_application.py | 12 +++ source/qre/src/trace.rs | 93 +++++++++++-------- source/qre/src/trace/tests.rs | 43 ++++++--- .../src/trace/transforms/lattice_surgery.rs | 3 +- source/qre/src/trace/transforms/psspc.rs | 50 +++++----- 8 files changed, 145 insertions(+), 92 deletions(-) diff --git a/source/qdk_package/qdk/qre/_qre.pyi b/source/qdk_package/qdk/qre/_qre.pyi index b425015c39..65bddcbe28 100644 --- a/source/qdk_package/qdk/qre/_qre.pyi +++ b/source/qdk_package/qdk/qre/_qre.pyi @@ -1431,12 +1431,12 @@ class Trace: """ ... - def add_block(self, repetitions: int = 1) -> Block: + def add_block(self, repetitions: float = 1) -> Block: """ Add a block to the trace. Args: - repetitions (int): The number of times the block is repeated. + repetitions (float): The number of times the block is repeated. Returns: Block: The block. @@ -1484,12 +1484,12 @@ class Block: """ ... - def add_block(self, repetitions: int = 1) -> Block: + def add_block(self, repetitions: float = 1) -> Block: """ Add a nested block to the block. Args: - repetitions (int): The number of times the block is repeated. + repetitions (float): The number of times the block is repeated. Returns: Block: The block. diff --git a/source/qdk_package/qdk/qre/interop/_cirq.py b/source/qdk_package/qdk/qre/interop/_cirq.py index 559621415a..71d8c233e8 100644 --- a/source/qdk_package/qdk/qre/interop/_cirq.py +++ b/source/qdk_package/qdk/qre/interop/_cirq.py @@ -3,7 +3,6 @@ from __future__ import annotations -import random from dataclasses import dataclass from enum import Enum from math import pi @@ -163,7 +162,7 @@ def __init__( ) ) - def push_block(self, repetitions: int): + def push_block(self, repetitions: float): """Open a new repeated block with the given number of repetitions.""" block = self.block.add_block(repetitions) self._blocks.append(block) @@ -279,7 +278,9 @@ def handle_op( for sub_op in gate._to_trace(self, op): # type: ignore self.handle_op(sub_op) elif hasattr(gate, "_decompose_with_context_"): - for sub_op in gate._decompose_with_context_(op.qubits, self.decomp_context): # type: ignore + for sub_op in gate._decompose_with_context_( + op.qubits, self.decomp_context + ): # type: ignore self.handle_op(sub_op) elif hasattr(gate, "_decompose_"): # decompose the gate and handle the resulting operations recursively @@ -289,8 +290,9 @@ def handle_op( for sub_op in op._decompose_with_context_(self.decomp_context): # type: ignore self.handle_op(sub_op) elif isinstance(op, ClassicallyControlledOperation): - if random.random() < self.classical_control_probability: - self.handle_op(op.without_classical_controls()) + self.push_block(self.classical_control_probability) + self.handle_op(op.without_classical_controls()) + self.pop_block() elif isinstance(op, cirq.CircuitOperation): if isinstance(op.repetitions, int): self.push_block(op.repetitions) @@ -318,7 +320,7 @@ class PushBlock: repetitions: Number of times the block is repeated. """ - repetitions: int + repetitions: float @dataclass(frozen=True, slots=True) @@ -595,9 +597,9 @@ def assert_qubits_type(qs: Sequence[cirq.Qid], qubit_type: QubitType) -> None: for q in qs: actual_type = _as_typed_qubit(q).qubit_type - assert ( - actual_type == qubit_type - ), f"{q} expected to be {qubit_type}, was {actual_type}." + assert actual_type == qubit_type, ( + f"{q} expected to be {qubit_type}, was {actual_type}." + ) class _TypedQubitManager(cirq.GreedyQubitManager): diff --git a/source/qdk_package/src/qre.rs b/source/qdk_package/src/qre.rs index 0d31168e38..7f0683ff06 100644 --- a/source/qdk_package/src/qre.rs +++ b/source/qdk_package/src/qre.rs @@ -1203,8 +1203,8 @@ impl Trace { } } - #[pyo3(signature = (repetitions = 1))] - pub fn add_block(mut slf: PyRefMut<'_, Self>, repetitions: u64) -> Block { + #[pyo3(signature = (repetitions = 1.0))] + pub fn add_block(mut slf: PyRefMut<'_, Self>, repetitions: f64) -> Block { let block = slf.0.add_block(repetitions); let ptr = NonNull::from(block); Block { @@ -1259,8 +1259,8 @@ impl Block { unsafe { self.ptr.as_mut() }.add_operation(id, qubits, params); } - #[pyo3(signature = (repetitions = 1))] - pub fn add_block(&mut self, py: Python<'_>, repetitions: u64) -> PyResult { + #[pyo3(signature = (repetitions = 1.0))] + pub fn add_block(&mut self, py: Python<'_>, repetitions: f64) -> PyResult { let block = unsafe { self.ptr.as_mut() }.add_block(repetitions); let ptr = NonNull::from(block); Ok(Block { diff --git a/source/qdk_package/tests/qre/test_application.py b/source/qdk_package/tests/qre/test_application.py index 95089209d5..bec171b4a2 100644 --- a/source/qdk_package/tests/qre/test_application.py +++ b/source/qdk_package/tests/qre/test_application.py @@ -82,6 +82,18 @@ def test_trace_properties(): assert isinstance(trace.get_property(STR), str) +def test_trace_accepts_fractional_block_repetitions(): + """Test that fractional block repetitions compose through the public Python API.""" + trace = Trace(1) + outer = trace.add_block(0.5) + inner = outer.add_block(0.5) + inner.add_operation(T, [0]) + + assert trace.depth == 1 + assert trace.num_gates == 1 + assert "repeat 0.5" in str(trace) + + def test_qsharp_application(): """Test QSharpApplication trace generation and estimation from a Q# program.""" code = """ diff --git a/source/qre/src/trace.rs b/source/qre/src/trace.rs index 6ee39b6367..9548f53430 100644 --- a/source/qre/src/trace.rs +++ b/source/qre/src/trace.rs @@ -73,7 +73,7 @@ impl Trace { self.block.add_operation(id, qubits, params); } - pub fn add_block(&mut self, repetitions: u64) -> &mut Block { + pub fn add_block(&mut self, repetitions: f64) -> &mut Block { self.block.add_block(repetitions) } @@ -169,9 +169,9 @@ impl Trace { #[allow(clippy::cast_precision_loss)] #[must_use] pub fn required_instruction_ids(&self, max_error: Option) -> ISARequirements { - let mut constraints = FxHashMap::::default(); + let mut constraints = FxHashMap::::default(); - let mut update_constraints = |id: u64, arity: u64, added_volume: u64| { + let mut update_constraints = |id: u64, arity: u64, added_volume: f64| { constraints .entry(id) .and_modify(|(constraint, volume)| { @@ -191,24 +191,22 @@ impl Trace { for (gate, mult) in self.deep_iter() { let arity = gate.qubits.len() as u64; - update_constraints(gate.id, arity, mult * arity); + update_constraints(gate.id, arity, mult * arity as f64); } if let Some(ref rs) = self.resource_states { for (res_id, count) in rs { - update_constraints(*res_id, 1, *count); + update_constraints(*res_id, 1, *count as f64); } } if let Some(memory_qubits) = self.memory_qubits { - update_constraints(instruction_ids::MEMORY, memory_qubits, memory_qubits); + update_constraints(instruction_ids::MEMORY, memory_qubits, memory_qubits as f64); } if let Some(max_error) = max_error { constraints .into_values() .map(|(mut c, volume)| { - c.set_error_rate(Some(ConstraintBound::less_equal( - max_error / (volume as f64), - ))); + c.set_error_rate(Some(ConstraintBound::less_equal(max_error / volume))); c }) .collect() @@ -224,17 +222,22 @@ impl Trace { #[must_use] pub fn num_gates(&self) -> u64 { - self.deep_iter().map(|(_, m)| m).sum() + round_up_to_u64( + self.deep_iter() + .map(|(_, multiplier)| multiplier) + .sum::(), + ) } pub fn runtime(&self, locked: &LockedISA) -> Result { - Ok(self - .block - .depth_and_used(Some(&|op: &Gate| { - let instr = get_instruction(locked, op.id)?; - Ok(instr.expect_time(Some(op.qubits.len() as u64))) - }))? - .0) + Ok(round_up_to_u64( + self.block + .depth_and_used(Some(&|op: &Gate| { + let instr = get_instruction(locked, op.id)?; + Ok(instr.expect_time(Some(op.qubits.len() as u64))) + }))? + .0, + )) } #[allow( @@ -302,7 +305,7 @@ impl Trace { qubit_counts.insert(i, qubit_count); } - let actual_error = result.add_error(rate * (mult as f64)); + let actual_error = result.add_error(rate * mult); if actual_error > max_error { return Err(Error::MaximumErrorExceeded { actual_error, @@ -428,6 +431,11 @@ impl Display for Trace { } } +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +pub(crate) fn round_up_to_u64(value: f64) -> u64 { + if value <= 0.0 { 0 } else { value.ceil() as u64 } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Operation { GateOperation(Gate), @@ -444,14 +452,14 @@ pub struct Gate { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Block { operations: Vec, - repetitions: u64, + repetitions: f64, } impl Default for Block { fn default() -> Self { Self { operations: Vec::new(), - repetitions: 1, + repetitions: 1.0, } } } @@ -462,7 +470,7 @@ impl Block { .push(Operation::gate_operation(id, qubits, params)); } - pub fn add_block(&mut self, repetitions: u64) -> &mut Block { + pub fn add_block(&mut self, repetitions: f64) -> &mut Block { self.operations .push(Operation::block_operation(repetitions)); @@ -474,7 +482,7 @@ impl Block { pub fn write(&self, f: &mut Formatter<'_>, indent: usize) -> std::fmt::Result { let indent_str = " ".repeat(indent); - if self.repetitions == 1 { + if self.repetitions == 1.0 { writeln!(f, "{indent_str}{{")?; } else { writeln!(f, "{indent_str}repeat {} {{", self.repetitions)?; @@ -517,8 +525,8 @@ impl Block { fn depth_and_used Result>( &self, duration_fn: Option<&FnDuration>, - ) -> Result<(u64, FxHashSet), Error> { - let mut qubit_depths: FxHashMap = FxHashMap::default(); + ) -> Result<(f64, FxHashSet), Error> { + let mut qubit_depths: FxHashMap = FxHashMap::default(); let mut all_used = FxHashSet::default(); for op in &self.operations { @@ -528,13 +536,13 @@ impl Block { .qubits .iter() .filter_map(|q| qubit_depths.get(q)) - .max() + .max_by(|left, right| left.total_cmp(right)) .copied() - .unwrap_or(0); + .unwrap_or(0.0); let duration = match duration_fn { - Some(f) => f(gate)?, - _ => 1, + Some(f) => f(gate)? as f64, + _ => 1.0, }; let end_time = start_time + duration; @@ -552,9 +560,9 @@ impl Block { let start_time = used .iter() .filter_map(|q| qubit_depths.get(q)) - .max() + .max_by(|left, right| left.total_cmp(right)) .copied() - .unwrap_or(0); + .unwrap_or(0.0); let end_time = start_time + duration; for q in &used { @@ -565,15 +573,22 @@ impl Block { } } - let max_depth = qubit_depths.values().max().copied().unwrap_or(0); - Ok((max_depth * self.repetitions, all_used)) + let max_depth = qubit_depths + .values() + .max_by(|left, right| left.total_cmp(right)); + Ok(( + max_depth.copied().unwrap_or(0.0) * self.repetitions, + all_used, + )) } #[must_use] pub fn depth(&self) -> u64 { - self.depth_and_used:: Result>(None) - .expect("Duration function is None") - .0 + round_up_to_u64( + self.depth_and_used:: Result>(None) + .expect("Duration function is None") + .0, + ) } } @@ -588,7 +603,7 @@ impl Operation { Operation::GateOperation(Gate { id, qubits, params }) } - fn block_operation(repetitions: u64) -> Self { + fn block_operation(repetitions: f64) -> Self { Operation::BlockOperation(Block { operations: Vec::new(), repetitions, @@ -597,19 +612,19 @@ impl Operation { } pub struct TraceIterator<'a> { - stack: Vec<(std::slice::Iter<'a, Operation>, u64)>, + stack: Vec<(std::slice::Iter<'a, Operation>, f64)>, } impl<'a> TraceIterator<'a> { fn new(block: &'a Block) -> Self { Self { - stack: vec![(block.operations.iter(), 1)], + stack: vec![(block.operations.iter(), 1.0)], } } } impl<'a> Iterator for TraceIterator<'a> { - type Item = (&'a Gate, u64); + type Item = (&'a Gate, f64); fn next(&mut self) -> Option { loop { diff --git a/source/qre/src/trace/tests.rs b/source/qre/src/trace/tests.rs index 3d083b290d..c2fab9b49a 100644 --- a/source/qre/src/trace/tests.rs +++ b/source/qre/src/trace/tests.rs @@ -17,15 +17,15 @@ fn test_trace_iteration() { fn test_nested_blocks() { let mut trace = Trace::new(3); trace.add_operation(1, vec![0], vec![]); - let block = trace.add_block(2); + let block = trace.add_block(2.0); block.add_operation(2, vec![1], vec![]); - let block = block.add_block(3); + let block = block.add_block(3.0); block.add_operation(3, vec![2], vec![]); trace.add_operation(1, vec![0], vec![]); let repetitions = trace.deep_iter().map(|(_, rep)| rep).collect::>(); assert_eq!(repetitions.len(), 4); - assert_eq!(repetitions, vec![1, 2, 6, 1]); + assert_eq!(repetitions, vec![1.0, 2.0, 6.0, 1.0]); } #[test] @@ -47,7 +47,7 @@ fn test_depth_with_blocks() { let mut trace = Trace::new(2); trace.add_operation(1, vec![0], vec![]); // Depth 1 on q0 - let block = trace.add_block(2); + let block = trace.add_block(2.0); block.add_operation(2, vec![1], vec![]); // Depth 1 on q1 * 2 reps = 2 // Block acts as barrier *only on qubits it touches*. @@ -65,10 +65,10 @@ fn test_depth_with_blocks() { fn test_depth_parallel_blocks() { let mut trace = Trace::new(4); - let block1 = trace.add_block(1); + let block1 = trace.add_block(1.0); block1.add_operation(1, vec![0], vec![]); // q0: 1 - let block2 = trace.add_block(1); + let block2 = trace.add_block(1.0); block2.add_operation(2, vec![1], vec![]); // q1: 1 // Blocks are parallel @@ -152,7 +152,7 @@ fn test_lattice_surgery_transform() { let (gate, mult) = ls_ops[0]; assert_eq!(gate.id, LATTICE_SURGERY); - assert_eq!(mult, 2); // Multiplier should carry the repetition count (depth) + assert_eq!(mult, 2.0); // Multiplier should carry the repetition count (depth) } #[test] @@ -263,7 +263,7 @@ fn test_trace_display_unknown_instruction() { #[test] fn test_block_display_with_repetitions() { let mut trace = Trace::new(1); - let block = trace.add_block(10); + let block = trace.add_block(10.0); block.add_operation(H, vec![0], vec![]); let display = format!("{trace}"); @@ -275,6 +275,25 @@ fn test_block_display_with_repetitions() { assert!(display.contains('H'), "Expected 'H' in block: {display}"); } +#[test] +fn test_fractional_block_repetitions_compose_before_rounding() { + let mut trace = Trace::new(1); + let outer = trace.add_block(0.5); + let inner = outer.add_block(0.5); + inner.add_operation(T, vec![0], vec![]); + + let repetitions = trace.deep_iter().map(|(_, rep)| rep).collect::>(); + assert_eq!(repetitions, vec![0.25]); + assert_eq!(trace.depth(), 1); + assert_eq!(trace.num_gates(), 1); + + let display = format!("{trace}"); + assert!( + display.contains("repeat 0.5"), + "Expected 'repeat 0.5' in: {display}" + ); +} + /// Helper to create an ISA with instructions that have known time values. /// Each entry is (id, arity, time). fn isa_with_times(entries: &[(u64, u64, u64)]) -> ISA { @@ -342,7 +361,7 @@ fn test_runtime_sequential_operations() { #[test] fn test_runtime_with_repeated_block() { let mut trace = Trace::new(1); - let block = trace.add_block(5); + let block = trace.add_block(5.0); block.add_operation(T, vec![0], vec![]); let isa = isa_with_times(&[(T, 1, 100)]); @@ -358,8 +377,8 @@ fn test_runtime_with_repeated_block() { #[test] fn test_runtime_nested_blocks() { let mut trace = Trace::new(1); - let outer = trace.add_block(3); - let inner = outer.add_block(2); + let outer = trace.add_block(3.0); + let inner = outer.add_block(2.0); inner.add_operation(H, vec![0], vec![]); let isa = isa_with_times(&[(H, 1, 10)]); @@ -423,7 +442,7 @@ fn test_runtime_empty_trace() { fn test_runtime_block_parallel_to_operation() { let mut trace = Trace::new(2); // Block on q0 - let block = trace.add_block(4); + let block = trace.add_block(4.0); block.add_operation(T, vec![0], vec![]); // Operation on q1 (parallel to block) trace.add_operation(H, vec![1], vec![]); diff --git a/source/qre/src/trace/transforms/lattice_surgery.rs b/source/qre/src/trace/transforms/lattice_surgery.rs index fd3ff45f72..3f813be6af 100644 --- a/source/qre/src/trace/transforms/lattice_surgery.rs +++ b/source/qre/src/trace/transforms/lattice_surgery.rs @@ -32,8 +32,7 @@ impl TraceTransform for LatticeSurgery { fn transform(&self, trace: &Trace) -> Result { let mut transformed = trace.clone_empty(None); - let block = - transformed.add_block((trace.depth() as f64 * self.slow_down_factor).ceil() as u64); + let block = transformed.add_block((trace.depth() as f64 * self.slow_down_factor).ceil()); block.add_operation( instruction_ids::LATTICE_SURGERY, (0..trace.compute_qubits()).collect(), diff --git a/source/qre/src/trace/transforms/psspc.rs b/source/qre/src/trace/transforms/psspc.rs index 80ec36bd99..f29ef97921 100644 --- a/source/qre/src/trace/transforms/psspc.rs +++ b/source/qre/src/trace/transforms/psspc.rs @@ -5,6 +5,8 @@ use crate::property_keys::NUM_TS_PER_ROTATION; use crate::trace::{Gate, TraceTransform}; use crate::{Error, Property, Trace, instruction_ids}; +use super::super::round_up_to_u64; + /// Implements the Parellel Synthesis Sequential Pauli Computation (PSSPC) /// layout algorithm described in Appendix D in /// [arXiv:2211.07629](https://arxiv.org/pdf/2211.07629). This scheme combines @@ -64,11 +66,10 @@ impl PSSPC { } impl PSSPC { - #[allow(clippy::cast_possible_truncation)] fn psspc_counts(trace: &Trace) -> Result { let mut counter = PSSPCCounts::default(); - let mut max_rotation_depth = vec![0; trace.total_qubits() as usize]; + let mut max_rotation_depth = vec![0.0; trace.total_qubits() as usize]; for (Gate { id, qubits, .. }, mult) in trace.deep_iter() { if instruction_ids::is_pauli_measurement(*id) { @@ -81,7 +82,7 @@ impl PSSPC { counter.rotation_like += mult; // Track rotation depth - let mut current_depth = 0; + let mut current_depth = 0.0; for q in qubits { if max_rotation_depth[*q as usize] > current_depth { current_depth = max_rotation_depth[*q as usize]; @@ -107,7 +108,7 @@ impl PSSPC { } else { // For Clifford gates, synchronize depths across qubits if !qubits.is_empty() { - let mut max_depth = 0; + let mut max_depth = 0.0; for q in qubits { if max_rotation_depth[*q as usize] > max_depth { max_depth = max_rotation_depth[*q as usize]; @@ -136,7 +137,7 @@ impl PSSPC { transformed.increment_resource_state(instruction_ids::T, t_states); transformed.increment_resource_state(instruction_ids::CCX, ccx_states); - let block = transformed.add_block(logical_depth); + let block = transformed.add_block(logical_depth as f64); block.add_operation( instruction_ids::MULTI_PAULI_MEAS, (0..logical_qubits).collect(), @@ -144,7 +145,7 @@ impl PSSPC { ); // Add error due to rotation synthesis - transformed.increment_base_error(counts.rotation_like as f64 * self.synthesis_error()); + transformed.increment_base_error(counts.rotation_like * self.synthesis_error()); // Track some properties transformed.set_property( @@ -172,12 +173,15 @@ impl PSSPC { /// sequence according to Eq. (D3) in /// [arXiv:2211.07629](https://arxiv.org/pdf/2211.07629) fn logical_depth_overhead(&self, counter: &PSSPCCounts) -> u64 { - (counter.measurements + counter.t_like + counter.rotation_like) - * self.num_measurements_per_r - + counter.ccx_like * self.num_measurements_per_ccx - + counter.read_from_memory * self.num_measurements_per_rfm - + counter.write_to_memory * self.num_measurements_per_wtm - + (self.num_ts_per_rotation * counter.rotation_depth) * self.num_measurements_per_r + round_up_to_u64( + (counter.measurements + counter.t_like + counter.rotation_like) + * self.num_measurements_per_r as f64 + + counter.ccx_like * self.num_measurements_per_ccx as f64 + + counter.read_from_memory * self.num_measurements_per_rfm as f64 + + counter.write_to_memory * self.num_measurements_per_wtm as f64 + + (self.num_ts_per_rotation as f64 * counter.rotation_depth) + * self.num_measurements_per_r as f64, + ) } /// Calculates the number of T and CCX magic states that are consumed by @@ -187,12 +191,14 @@ impl PSSPC { /// CCX magic states are only counted if the hyper parameter /// `ccx_magic_states` is set to true. fn num_magic_states(&self, counter: &PSSPCCounts) -> (u64, u64) { - let t_states = counter.t_like + self.num_ts_per_rotation * counter.rotation_like; + let t_states = round_up_to_u64( + counter.t_like + self.num_ts_per_rotation as f64 * counter.rotation_like, + ); if self.ccx_magic_states { - (t_states, counter.ccx_like) + (t_states, round_up_to_u64(counter.ccx_like)) } else { - (t_states + 4 * counter.ccx_like, 0) + (t_states + round_up_to_u64(4.0 * counter.ccx_like), 0) } } @@ -215,11 +221,11 @@ impl TraceTransform for PSSPC { #[derive(Default)] struct PSSPCCounts { - measurements: u64, - t_like: u64, - ccx_like: u64, - rotation_like: u64, - write_to_memory: u64, - read_from_memory: u64, - rotation_depth: u64, + measurements: f64, + t_like: f64, + ccx_like: f64, + rotation_like: f64, + write_to_memory: f64, + read_from_memory: f64, + rotation_depth: f64, } From ff39c8db44e409d350e12c0e58da683acd8d76b2 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 16 Jun 2026 18:02:58 -0700 Subject: [PATCH 2/4] more type changes --- source/qdk_package/qdk/qre/_qre.pyi | 48 +++++------ source/qdk_package/src/qre.rs | 26 +++--- source/qre/src/lib.rs | 2 +- source/qre/src/pareto/tests.rs | 8 +- source/qre/src/result.rs | 26 +++--- source/qre/src/trace.rs | 79 ++++++++----------- source/qre/src/trace/tests.rs | 58 +++++++------- .../src/trace/transforms/lattice_surgery.rs | 7 +- source/qre/src/trace/transforms/psspc.rs | 30 +++---- 9 files changed, 132 insertions(+), 152 deletions(-) diff --git a/source/qdk_package/qdk/qre/_qre.pyi b/source/qdk_package/qdk/qre/_qre.pyi index 65bddcbe28..8a31065409 100644 --- a/source/qdk_package/qdk/qre/_qre.pyi +++ b/source/qdk_package/qdk/qre/_qre.pyi @@ -909,14 +909,14 @@ class EstimationResult: """ def __new__( - cls, *, qubits: int = 0, runtime: int = 0, error: float = 0.0 + cls, *, qubits: int = 0, runtime: float = 0.0, error: float = 0.0 ) -> EstimationResult: """ Create a new estimation result. Args: qubits (int): The number of logical qubits. - runtime (int): The runtime in nanoseconds. + runtime (float): The runtime in nanoseconds. error (float): The error probability of the computation. Returns: @@ -945,22 +945,22 @@ class EstimationResult: ... @property - def runtime(self) -> int: + def runtime(self) -> float: """ The runtime in nanoseconds. Returns: - int: The runtime in nanoseconds. + float: The runtime in nanoseconds. """ ... @runtime.setter - def runtime(self, runtime: int) -> None: + def runtime(self, runtime: float) -> None: """ Set the runtime. Args: - runtime (int): The runtime in nanoseconds to set. + runtime (float): The runtime in nanoseconds to set. """ ... @@ -1098,13 +1098,13 @@ class _EstimationCollection: ... @property - def all_summaries(self) -> list[tuple[int, int, int, int]]: + def all_summaries(self) -> list[tuple[int, int, int, float]]: """ Return lightweight summaries of ALL successful estimates as a list of (trace_index, isa_index, qubits, runtime) tuples. Returns: - list[tuple[int, int, int, int]]: List of (trace_index, isa_index, + list[tuple[int, int, int, float]]: List of (trace_index, isa_index, qubits, runtime) for every successful estimation. """ ... @@ -1125,22 +1125,22 @@ class FactoryResult: """ @property - def copies(self) -> int: + def copies(self) -> float: """ The number of factory copies. Returns: - int: The number of factory copies. + float: The number of factory copies. """ ... @property - def runs(self) -> int: + def runs(self) -> float: """ The number of factory runs. Returns: - int: The number of factory runs. + float: The number of factory runs. """ ... @@ -1155,12 +1155,12 @@ class FactoryResult: ... @property - def states(self) -> int: + def states(self) -> float: """ The number of states produced by the factory. Returns: - int: The number of states produced by the factory. + float: The number of states produced by the factory. """ ... @@ -1294,13 +1294,13 @@ class Trace: """ ... - def increment_resource_state(self, resource_id: int, amount: int) -> None: + def increment_resource_state(self, resource_id: int, amount: float) -> None: """ Increments a resource state count. Args: resource_id (int): The resource state ID. - amount (int): The amount to increment. + amount (float): The amount to increment. """ ... @@ -1351,16 +1351,16 @@ class Trace: ... @property - def depth(self) -> int: + def depth(self) -> float: """ The trace depth. Returns: - int: The trace depth. + float: The trace depth. """ ... - def runtime(self, isa: ISA) -> Optional[int]: + def runtime(self, isa: ISA) -> Optional[float]: """ The trace runtime in nanoseconds for a given ISA. @@ -1368,17 +1368,17 @@ class Trace: isa (ISA): The ISA to compute the runtime for. Returns: - Optional[int]: The trace runtime in nanoseconds, or None if it + Optional[float]: The trace runtime in nanoseconds, or None if it cannot be computed. """ @property - def num_gates(self) -> int: + def num_gates(self) -> float: """ The total number of gates in the trace. Returns: - int: The total number of gates. + float: The total number of gates. """ ... @@ -1400,12 +1400,12 @@ class Trace: ... # The implementation in Rust returns Option, so it fits @property - def resource_states(self) -> dict[int, int]: + def resource_states(self) -> dict[int, float]: """ The resource states used in the trace. Returns: - dict[int, int]: A dictionary mapping instruction IDs to their counts. + dict[int, float]: A dictionary mapping instruction IDs to their counts. """ ... diff --git a/source/qdk_package/src/qre.rs b/source/qdk_package/src/qre.rs index 7f0683ff06..235c65fac4 100644 --- a/source/qdk_package/src/qre.rs +++ b/source/qdk_package/src/qre.rs @@ -886,7 +886,7 @@ impl EstimationCollection { /// Returns lightweight summaries of ALL successful estimates as a list /// of (trace index, isa index, qubits, runtime) tuples. #[getter] - pub fn all_summaries(&self) -> Vec<(usize, usize, u64, u64)> { + pub fn all_summaries(&self) -> Vec<(usize, usize, u64, f64)> { self.0 .all_summaries() .iter() @@ -932,8 +932,8 @@ pub struct EstimationResult(qre::EstimationResult); #[pymethods] impl EstimationResult { #[new] - #[pyo3(signature = (*, qubits = 0, runtime = 0, error = 0.0))] - pub fn new(qubits: u64, runtime: u64, error: f64) -> Self { + #[pyo3(signature = (*, qubits = 0, runtime = 0.0, error = 0.0))] + pub fn new(qubits: u64, runtime: f64, error: f64) -> Self { let mut result = qre::EstimationResult::new(); result.add_qubits(qubits); result.add_runtime(runtime); @@ -953,12 +953,12 @@ impl EstimationResult { } #[getter] - pub fn runtime(&self) -> u64 { + pub fn runtime(&self) -> f64 { self.0.runtime() } #[setter] - pub fn set_runtime(&mut self, runtime: u64) { + pub fn set_runtime(&mut self, runtime: f64) { self.0.set_runtime(runtime); } @@ -1033,17 +1033,17 @@ pub struct FactoryResult(qre::FactoryResult); #[pymethods] impl FactoryResult { #[getter] - pub fn copies(&self) -> u64 { + pub fn copies(&self) -> f64 { self.0.copies() } #[getter] - pub fn runs(&self) -> u64 { + pub fn runs(&self) -> f64 { self.0.runs() } #[getter] - pub fn states(&self) -> u64 { + pub fn states(&self) -> f64 { self.0.states() } @@ -1150,7 +1150,7 @@ impl Trace { let dict = PyDict::new(self_.py()); if let Some(resource_states) = self_.0.get_resource_states() { for (resource_id, count) in resource_states { - if *count != 0 { + if *count != 0.0 { dict.set_item(resource_id, *count)?; } } @@ -1164,17 +1164,17 @@ impl Trace { } #[getter] - pub fn depth(&self) -> u64 { + pub fn depth(&self) -> f64 { self.0.depth() } - pub fn runtime(&self, isa: &ISA) -> Option { + pub fn runtime(&self, isa: &ISA) -> Option { let locked = isa.0.lock(); self.0.runtime(&locked).ok() } #[getter] - pub fn num_gates(&self) -> u64 { + pub fn num_gates(&self) -> f64 { self.0.num_gates() } @@ -1231,7 +1231,7 @@ impl Trace { self.0.increment_memory_qubits(amount); } - pub fn increment_resource_state(&mut self, resource_id: u64, amount: u64) { + pub fn increment_resource_state(&mut self, resource_id: u64, amount: f64) { self.0.increment_resource_state(resource_id, amount); } diff --git a/source/qre/src/lib.rs b/source/qre/src/lib.rs index ea52480994..526afb518f 100644 --- a/source/qre/src/lib.rs +++ b/source/qre/src/lib.rs @@ -58,7 +58,7 @@ pub enum Error { FactoryTimeExceedsAlgorithmRuntime { id: u64, factory_time: u64, - algorithm_runtime: u64, + algorithm_runtime: f64, }, /// Unsupported instruction in trace transformation #[error("unsupported instruction {} in trace transformation '{name}'", instruction_name(*id).unwrap_or(&id.to_string()))] diff --git a/source/qre/src/pareto/tests.rs b/source/qre/src/pareto/tests.rs index eaf2539c33..540ffcce1d 100644 --- a/source/qre/src/pareto/tests.rs +++ b/source/qre/src/pareto/tests.rs @@ -155,15 +155,15 @@ fn test_update_frontier_3d() { fn test_estimation_results() { let mut result_worst = EstimationResult::new(); result_worst.add_qubits(994_570); - result_worst.add_runtime(346_196_523_750); + result_worst.add_runtime(346_196_523_750.0); let mut result_mid = EstimationResult::new(); result_mid.add_qubits(994_570); - result_mid.add_runtime(346_191_476_400); + result_mid.add_runtime(346_191_476_400.0); let mut result_best = EstimationResult::new(); result_best.add_qubits(994_570); - result_best.add_runtime(346_181_381_700); + result_best.add_runtime(346_181_381_700.0); let results = [result_worst, result_mid, result_best]; let permutations = [ @@ -186,7 +186,7 @@ fn test_estimation_results() { let item = frontier.iter().next().expect("has item"); assert_eq!( item.runtime(), - 346_181_381_700, + 346_181_381_700.0, "Wrong item retained for permutation {p:?}", ); } diff --git a/source/qre/src/result.rs b/source/qre/src/result.rs index 208195531b..da7b6a914a 100644 --- a/source/qre/src/result.rs +++ b/source/qre/src/result.rs @@ -13,7 +13,7 @@ use crate::{ISA, ParetoFrontier2D, ParetoItem2D, Property}; #[derive(Clone, Default)] pub struct EstimationResult { qubits: u64, - runtime: u64, + runtime: f64, error: f64, factories: FxHashMap, isa: ISA, @@ -34,7 +34,7 @@ impl EstimationResult { } #[must_use] - pub fn runtime(&self) -> u64 { + pub fn runtime(&self) -> f64 { self.runtime } @@ -52,7 +52,7 @@ impl EstimationResult { self.qubits = qubits; } - pub fn set_runtime(&mut self, runtime: u64) { + pub fn set_runtime(&mut self, runtime: f64) { self.runtime = runtime; } @@ -67,7 +67,7 @@ impl EstimationResult { } /// Adds to the current runtime and returns the new value. - pub fn add_runtime(&mut self, runtime: u64) -> u64 { + pub fn add_runtime(&mut self, runtime: f64) -> f64 { self.runtime += runtime; self.runtime } @@ -154,7 +154,7 @@ impl Display for EstimationResult { impl ParetoItem2D for EstimationResult { type Objective1 = u64; // qubits - type Objective2 = u64; // runtime + type Objective2 = f64; // runtime fn objective1(&self) -> Self::Objective1 { self.qubits @@ -172,7 +172,7 @@ pub struct ResultSummary { pub trace_index: usize, pub isa_index: usize, pub qubits: u64, - pub runtime: u64, + pub runtime: f64, } #[derive(Default)] @@ -249,15 +249,15 @@ impl DerefMut for EstimationCollection { #[derive(Clone)] pub struct FactoryResult { - copies: u64, - runs: u64, - states: u64, + copies: f64, + runs: f64, + states: f64, error_rate: f64, } impl FactoryResult { #[must_use] - pub fn new(copies: u64, runs: u64, states: u64, error_rate: f64) -> Self { + pub fn new(copies: f64, runs: f64, states: f64, error_rate: f64) -> Self { Self { copies, runs, @@ -267,17 +267,17 @@ impl FactoryResult { } #[must_use] - pub fn copies(&self) -> u64 { + pub fn copies(&self) -> f64 { self.copies } #[must_use] - pub fn runs(&self) -> u64 { + pub fn runs(&self) -> f64 { self.runs } #[must_use] - pub fn states(&self) -> u64 { + pub fn states(&self) -> f64 { self.states } diff --git a/source/qre/src/trace.rs b/source/qre/src/trace.rs index 9548f53430..6124175063 100644 --- a/source/qre/src/trace.rs +++ b/source/qre/src/trace.rs @@ -35,7 +35,7 @@ pub struct Trace { base_error: f64, compute_qubits: u64, memory_qubits: Option, - resource_states: Option>, + resource_states: Option>, properties: FxHashMap, } @@ -117,8 +117,8 @@ impl Trace { self.compute_qubits + self.memory_qubits.unwrap_or(0) } - pub fn increment_resource_state(&mut self, resource_id: u64, amount: u64) { - if amount == 0 { + pub fn increment_resource_state(&mut self, resource_id: u64, amount: f64) { + if amount <= 0.0 { return; } let states = self.resource_states.get_or_insert_with(FxHashMap::default); @@ -126,18 +126,18 @@ impl Trace { } #[must_use] - pub fn get_resource_states(&self) -> Option<&FxHashMap> { + pub fn get_resource_states(&self) -> Option<&FxHashMap> { self.resource_states.as_ref() } #[must_use] - pub fn get_resource_state_count(&self, resource_id: u64) -> u64 { + pub fn get_resource_state_count(&self, resource_id: u64) -> f64 { if let Some(states) = &self.resource_states && let Some(count) = states.get(&resource_id) { return *count; } - 0 + 0.0 } pub fn set_property(&mut self, key: u64, value: Property) { @@ -195,7 +195,7 @@ impl Trace { } if let Some(ref rs) = self.resource_states { for (res_id, count) in rs { - update_constraints(*res_id, 1, *count as f64); + update_constraints(*res_id, 1, *count); } } if let Some(memory_qubits) = self.memory_qubits { @@ -216,28 +216,25 @@ impl Trace { } #[must_use] - pub fn depth(&self) -> u64 { + pub fn depth(&self) -> f64 { self.block.depth() } #[must_use] - pub fn num_gates(&self) -> u64 { - round_up_to_u64( - self.deep_iter() - .map(|(_, multiplier)| multiplier) - .sum::(), - ) - } - - pub fn runtime(&self, locked: &LockedISA) -> Result { - Ok(round_up_to_u64( - self.block - .depth_and_used(Some(&|op: &Gate| { - let instr = get_instruction(locked, op.id)?; - Ok(instr.expect_time(Some(op.qubits.len() as u64))) - }))? - .0, - )) + pub fn num_gates(&self) -> f64 { + self.deep_iter() + .map(|(_, multiplier)| multiplier) + .sum::() + } + + pub fn runtime(&self, locked: &LockedISA) -> Result { + Ok(self + .block + .depth_and_used(Some(&|op: &Gate| { + let instr = get_instruction(locked, op.id)?; + Ok(instr.expect_time(Some(op.qubits.len() as u64))) + }))? + .0) } #[allow( @@ -263,7 +260,7 @@ impl Trace { result.add_error(self.base_error); // Counts how many magic state factories are needed per resource state ID - let mut factories: FxHashMap = FxHashMap::default(); + let mut factories: FxHashMap = FxHashMap::default(); // This will track the number of physical qubits per logical qubit while // processing all the instructions. Normally, we assume that the number @@ -276,7 +273,7 @@ impl Trace { if let Some(resource_states) = &self.resource_states { for (state_id, count) in resource_states { let rate = get_error_rate_by_id(&locked, *state_id)?; - let actual_error = result.add_error(rate * (*count as f64)); + let actual_error = result.add_error(rate * *count); if actual_error > max_error { return Err(Error::MaximumErrorExceeded { actual_error, @@ -335,9 +332,9 @@ impl Trace { let factory_time = get_time(instr)?; let factory_space = get_space(instr)?; let factory_error_rate = get_error_rate(instr)?; - let runs = result.runtime() / factory_time; + let runs = result.runtime() / factory_time as f64; - if runs == 0 { + if runs <= 0.0 { return Err(Error::FactoryTimeExceedsAlgorithmRuntime { id: *factory, factory_time, @@ -345,9 +342,9 @@ impl Trace { }); } - let copies = count.div_ceil(runs); + let copies = count / runs; - total_factory_qubits += copies * factory_space; + total_factory_qubits += (copies * factory_space as f64).ceil() as u64; result.add_factory_result( *factory, FactoryResult::new(copies, runs, *count, factory_error_rate), @@ -375,9 +372,10 @@ impl Trace { // The number of rounds for the memory qubits to stay alive with // respect to the total runtime of the algorithm. - let rounds = result + let rounds = (result .runtime() - .div_ceil(memory.expect_time(Some(memory_qubits))); + / memory.expect_time(Some(memory_qubits)) as f64) + .ceil() as u64; let actual_error = result.add_error(rounds as f64 * memory.expect_error_rate(Some(memory_qubits))); @@ -431,11 +429,6 @@ impl Display for Trace { } } -#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] -pub(crate) fn round_up_to_u64(value: f64) -> u64 { - if value <= 0.0 { 0 } else { value.ceil() as u64 } -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Operation { GateOperation(Gate), @@ -583,12 +576,10 @@ impl Block { } #[must_use] - pub fn depth(&self) -> u64 { - round_up_to_u64( - self.depth_and_used:: Result>(None) - .expect("Duration function is None") - .0, - ) + pub fn depth(&self) -> f64 { + self.depth_and_used:: Result>(None) + .expect("Duration function is None") + .0 } } diff --git a/source/qre/src/trace/tests.rs b/source/qre/src/trace/tests.rs index c2fab9b49a..8ca1151a26 100644 --- a/source/qre/src/trace/tests.rs +++ b/source/qre/src/trace/tests.rs @@ -35,11 +35,11 @@ fn test_depth_simple() { trace.add_operation(2, vec![1], vec![]); // Operations are parallel - assert_eq!(trace.depth(), 1); + assert_eq!(trace.depth(), 1.0); trace.add_operation(3, vec![0], vec![]); // Operation on qubit 0 is sequential to first one - assert_eq!(trace.depth(), 2); + assert_eq!(trace.depth(), 2.0); } #[test] @@ -58,7 +58,7 @@ fn test_depth_with_blocks() { trace.add_operation(3, vec![0], vec![]); // Next op starts at depth 1 (after op 1). Ends at 2. - assert_eq!(trace.depth(), 2); + assert_eq!(trace.depth(), 2.0); } #[test] @@ -72,12 +72,12 @@ fn test_depth_parallel_blocks() { block2.add_operation(2, vec![1], vec![]); // q1: 1 // Blocks are parallel - assert_eq!(trace.depth(), 1); + assert_eq!(trace.depth(), 1.0); trace.add_operation(3, vec![0, 1], vec![]); // Dependent on q0 (1) and q1 (1). Start at 1. End at 2. - assert_eq!(trace.depth(), 2); + assert_eq!(trace.depth(), 2.0); } #[test] @@ -88,10 +88,10 @@ fn test_depth_entangled() { trace.add_operation(3, vec![0, 1], vec![]); // q0, q1 synced at 1 -> end at 2 - assert_eq!(trace.depth(), 2); + assert_eq!(trace.depth(), 2.0); trace.add_operation(4, vec![0], vec![]); // q0: 3 - assert_eq!(trace.depth(), 3); + assert_eq!(trace.depth(), 3.0); } #[test] @@ -113,10 +113,10 @@ fn test_psspc_transform() { let transformed = psspc.transform(&trace).expect("Transformation failed"); assert_eq!(transformed.compute_qubits(), 12); - assert_eq!(transformed.depth(), 47); + assert_eq!(transformed.depth(), 47.0); - assert_eq!(transformed.get_resource_state_count(T), 41); - assert_eq!(transformed.get_resource_state_count(CCX), 1); + assert_eq!(transformed.get_resource_state_count(T), 41.0); + assert_eq!(transformed.get_resource_state_count(CCX), 1.0); assert!(transformed.base_error() > 0.0); // Error is roughly 5e-9 for 20 Ts @@ -133,13 +133,13 @@ fn test_lattice_surgery_transform() { trace.add_operation(CX, vec![1, 2], vec![]); trace.add_operation(T, vec![0], vec![]); - assert_eq!(trace.depth(), 2); + assert_eq!(trace.depth(), 2.0); let ls = LatticeSurgery::default(); let transformed = ls.transform(&trace).expect("Transformation failed"); assert_eq!(transformed.compute_qubits(), 3); - assert_eq!(transformed.depth(), 2); + assert_eq!(transformed.depth(), 2.0); // Check that we have a LATTICE_SURGERY operation // TraceIterator visits the operation definition once, but with a multiplier. @@ -175,7 +175,7 @@ fn test_estimate_simple() { let result = trace.estimate(&isa, None).expect("Estimation failed"); assert!((result.error() - 0.001).abs() <= f64::EPSILON); - assert_eq!(result.runtime(), 100); + assert_eq!(result.runtime(), 100.0); assert_eq!(result.qubits(), 50); } @@ -183,7 +183,7 @@ fn test_estimate_simple() { fn test_estimate_with_factory() { let mut trace = Trace::new(1); // Algorithm needs 1000 T states - trace.increment_resource_state(T, 1000); + trace.increment_resource_state(T, 1000.0); // Some compute runtime to allow factories to run trace.add_operation(GENERIC, vec![0], vec![]); @@ -214,13 +214,13 @@ fn test_estimate_with_factory() { let result = trace.estimate(&isa, None).expect("Estimation failed"); - assert_eq!(result.runtime(), 1000); + assert_eq!(result.runtime(), 1000.0); assert_eq!(result.qubits(), 700); // Check factory result let factory_res = result.factories().get(&T).expect("Factory missing"); - assert_eq!(factory_res.copies(), 10); - assert_eq!(factory_res.runs(), 100); + assert_eq!(factory_res.copies(), 10.0); + assert_eq!(factory_res.runs(), 100.0); assert_eq!(result.factories().len(), 1); } @@ -284,8 +284,8 @@ fn test_fractional_block_repetitions_compose_before_rounding() { let repetitions = trace.deep_iter().map(|(_, rep)| rep).collect::>(); assert_eq!(repetitions, vec![0.25]); - assert_eq!(trace.depth(), 1); - assert_eq!(trace.num_gates(), 1); + assert_eq!(trace.depth(), 0.25); + assert_eq!(trace.num_gates(), 0.25); let display = format!("{trace}"); assert!( @@ -322,7 +322,7 @@ fn test_runtime_single_operation() { assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 100 + 100.0 ); } @@ -338,7 +338,7 @@ fn test_runtime_parallel_operations() { // Parallel: runtime is the max of the two = 100 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 100 + 100.0 ); } @@ -354,7 +354,7 @@ fn test_runtime_sequential_operations() { // Sequential on qubit 0: 100 + 50 = 150 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 150 + 150.0 ); } @@ -370,7 +370,7 @@ fn test_runtime_with_repeated_block() { // Block depth = 100, repeated 5 times = 500 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 500 + 500.0 ); } @@ -387,7 +387,7 @@ fn test_runtime_nested_blocks() { // Inner: 10 * 2 = 20, outer: 20 * 3 = 60 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 60 + 60.0 ); } @@ -401,7 +401,7 @@ fn test_runtime_multi_qubit_gate() { assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 200 + 200.0 ); } @@ -421,7 +421,7 @@ fn test_runtime_sequential_after_multi_qubit() { // max = 300 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 300 + 300.0 ); } @@ -434,7 +434,7 @@ fn test_runtime_empty_trace() { assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 0 + 0.0 ); } @@ -455,7 +455,7 @@ fn test_runtime_block_parallel_to_operation() { // max = 50 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 50 + 50.0 ); } @@ -491,6 +491,6 @@ fn test_runtime_mixed_sequential_and_parallel() { // max = 300 assert_eq!( trace.runtime(&locked).expect("runtime computation failed"), - 300 + 300.0 ); } diff --git a/source/qre/src/trace/transforms/lattice_surgery.rs b/source/qre/src/trace/transforms/lattice_surgery.rs index 3f813be6af..ac93286653 100644 --- a/source/qre/src/trace/transforms/lattice_surgery.rs +++ b/source/qre/src/trace/transforms/lattice_surgery.rs @@ -24,15 +24,10 @@ impl LatticeSurgery { } impl TraceTransform for LatticeSurgery { - #[allow( - clippy::cast_possible_truncation, - clippy::cast_precision_loss, - clippy::cast_sign_loss - )] fn transform(&self, trace: &Trace) -> Result { let mut transformed = trace.clone_empty(None); - let block = transformed.add_block((trace.depth() as f64 * self.slow_down_factor).ceil()); + let block = transformed.add_block((trace.depth() * self.slow_down_factor).ceil()); block.add_operation( instruction_ids::LATTICE_SURGERY, (0..trace.compute_qubits()).collect(), diff --git a/source/qre/src/trace/transforms/psspc.rs b/source/qre/src/trace/transforms/psspc.rs index f29ef97921..2295c49695 100644 --- a/source/qre/src/trace/transforms/psspc.rs +++ b/source/qre/src/trace/transforms/psspc.rs @@ -5,8 +5,6 @@ use crate::property_keys::NUM_TS_PER_ROTATION; use crate::trace::{Gate, TraceTransform}; use crate::{Error, Property, Trace, instruction_ids}; -use super::super::round_up_to_u64; - /// Implements the Parellel Synthesis Sequential Pauli Computation (PSSPC) /// layout algorithm described in Appendix D in /// [arXiv:2211.07629](https://arxiv.org/pdf/2211.07629). This scheme combines @@ -137,7 +135,7 @@ impl PSSPC { transformed.increment_resource_state(instruction_ids::T, t_states); transformed.increment_resource_state(instruction_ids::CCX, ccx_states); - let block = transformed.add_block(logical_depth as f64); + let block = transformed.add_block(logical_depth); block.add_operation( instruction_ids::MULTI_PAULI_MEAS, (0..logical_qubits).collect(), @@ -172,16 +170,14 @@ impl PSSPC { /// Calculates the number of multi-qubit Pauli measurements executed in /// sequence according to Eq. (D3) in /// [arXiv:2211.07629](https://arxiv.org/pdf/2211.07629) - fn logical_depth_overhead(&self, counter: &PSSPCCounts) -> u64 { - round_up_to_u64( - (counter.measurements + counter.t_like + counter.rotation_like) + fn logical_depth_overhead(&self, counter: &PSSPCCounts) -> f64 { + (counter.measurements + counter.t_like + counter.rotation_like) + * self.num_measurements_per_r as f64 + + counter.ccx_like * self.num_measurements_per_ccx as f64 + + counter.read_from_memory * self.num_measurements_per_rfm as f64 + + counter.write_to_memory * self.num_measurements_per_wtm as f64 + + (self.num_ts_per_rotation as f64 * counter.rotation_depth) * self.num_measurements_per_r as f64 - + counter.ccx_like * self.num_measurements_per_ccx as f64 - + counter.read_from_memory * self.num_measurements_per_rfm as f64 - + counter.write_to_memory * self.num_measurements_per_wtm as f64 - + (self.num_ts_per_rotation as f64 * counter.rotation_depth) - * self.num_measurements_per_r as f64, - ) } /// Calculates the number of T and CCX magic states that are consumed by @@ -190,15 +186,13 @@ impl PSSPC { /// /// CCX magic states are only counted if the hyper parameter /// `ccx_magic_states` is set to true. - fn num_magic_states(&self, counter: &PSSPCCounts) -> (u64, u64) { - let t_states = round_up_to_u64( - counter.t_like + self.num_ts_per_rotation as f64 * counter.rotation_like, - ); + fn num_magic_states(&self, counter: &PSSPCCounts) -> (f64, f64) { + let t_states = counter.t_like + self.num_ts_per_rotation as f64 * counter.rotation_like; if self.ccx_magic_states { - (t_states, round_up_to_u64(counter.ccx_like)) + (t_states, counter.ccx_like) } else { - (t_states + round_up_to_u64(4.0 * counter.ccx_like), 0) + (t_states + 4.0 * counter.ccx_like, 0.0) } } From 7e14f76d76302ee1666adc65bba0ef5dd8da0c51 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 16 Jun 2026 18:40:42 -0700 Subject: [PATCH 3/4] format --- source/qre/src/trace.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/source/qre/src/trace.rs b/source/qre/src/trace.rs index 38fea1094a..438326d86f 100644 --- a/source/qre/src/trace.rs +++ b/source/qre/src/trace.rs @@ -389,10 +389,8 @@ impl Trace { // The number of rounds for the memory qubits to stay alive with // respect to the total runtime of the algorithm. - let rounds = (result - .runtime() - / memory.expect_time(Some(memory_qubits)) as f64) - .ceil() as u64; + let rounds = + (result.runtime() / memory.expect_time(Some(memory_qubits)) as f64).ceil() as u64; let actual_error = result.add_error(rounds as f64 * memory.expect_error_rate(Some(memory_qubits))); From 1e0b11092c3bb6edad31f5e6688c81f3b8125d30 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 16 Jun 2026 18:46:05 -0700 Subject: [PATCH 4/4] update tests --- source/qre/src/trace.rs | 8 +++--- source/qre/src/trace/tests.rs | 14 +++++----- .../transforms/dynamic_memory_compute.rs | 2 +- .../dynamic_memory_compute/tests.rs | 28 +++++++++---------- source/qre/src/trace/transforms/unmemory.rs | 2 +- .../src/trace/transforms/unmemory/tests.rs | 6 ++-- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/source/qre/src/trace.rs b/source/qre/src/trace.rs index 438326d86f..5d663469ef 100644 --- a/source/qre/src/trace.rs +++ b/source/qre/src/trace.rs @@ -236,7 +236,7 @@ impl Trace { } #[must_use] - pub fn gate_counts(&self) -> FxHashMap { + pub fn gate_counts(&self) -> FxHashMap { let mut counts = FxHashMap::default(); for (gate, mult) in self.deep_iter() { *counts.entry(gate.id).or_default() += mult; @@ -670,7 +670,7 @@ impl<'a> Iterator for TraceIterator<'a> { pub struct WalkIterator<'a> { // Each frame: (operations slice, current index, remaining repetitions) - stack: Vec<(&'a [Operation], usize, u64)>, + stack: Vec<(&'a [Operation], usize, f64)>, } impl<'a> WalkIterator<'a> { @@ -697,8 +697,8 @@ impl<'a> Iterator for WalkIterator<'a> { } } } else { - *remaining -= 1; - if *remaining > 0 { + *remaining -= 1.0; + if *remaining > 0.0 { *idx = 0; } else { self.stack.pop(); diff --git a/source/qre/src/trace/tests.rs b/source/qre/src/trace/tests.rs index cb600216ef..89828661f8 100644 --- a/source/qre/src/trace/tests.rs +++ b/source/qre/src/trace/tests.rs @@ -42,7 +42,7 @@ fn test_walk_iter_simple() { fn test_walk_iter_with_block() { let mut trace = Trace::new(2); trace.add_operation(1, vec![0], vec![]); - let block = trace.add_block(3); + let block = trace.add_block(3.0); block.add_operation(2, vec![1], vec![]); trace.add_operation(3, vec![0], vec![]); @@ -54,9 +54,9 @@ fn test_walk_iter_with_block() { fn test_walk_iter_nested_blocks() { let mut trace = Trace::new(3); trace.add_operation(1, vec![0], vec![]); - let block = trace.add_block(2); + let block = trace.add_block(2.0); block.add_operation(2, vec![1], vec![]); - let inner = block.add_block(3); + let inner = block.add_block(3.0); inner.add_operation(3, vec![2], vec![]); trace.add_operation(4, vec![0], vec![]); @@ -70,15 +70,15 @@ fn test_walk_iter_nested_blocks() { fn test_walk_iter_count_matches_deep_iter() { let mut trace = Trace::new(3); trace.add_operation(1, vec![0], vec![]); - let block = trace.add_block(2); + let block = trace.add_block(2.0); block.add_operation(2, vec![1], vec![]); - let inner = block.add_block(3); + let inner = block.add_block(3.0); inner.add_operation(3, vec![2], vec![]); trace.add_operation(4, vec![0], vec![]); let walk_count = trace.walk_iter().count(); - let deep_count: u64 = trace.deep_iter().map(|(_, m)| m).sum(); - assert_eq!(walk_count as u64, deep_count); + let deep_count: f64 = trace.deep_iter().map(|(_, m)| m).sum(); + assert_eq!(walk_count as f64, deep_count); } #[test] diff --git a/source/qre/src/trace/transforms/dynamic_memory_compute.rs b/source/qre/src/trace/transforms/dynamic_memory_compute.rs index 92b4482d7e..ec1f897891 100644 --- a/source/qre/src/trace/transforms/dynamic_memory_compute.rs +++ b/source/qre/src/trace/transforms/dynamic_memory_compute.rs @@ -366,7 +366,7 @@ fn process_block( // qubits are read from memory. This preserves the // compact block representation while producing accurate // per-iteration counts. - let entry = if inner.repetitions > 1 { + let entry = if inner.repetitions > 1.0 { Some(state.clone()) } else { None diff --git a/source/qre/src/trace/transforms/dynamic_memory_compute/tests.rs b/source/qre/src/trace/transforms/dynamic_memory_compute/tests.rs index 235b95ebe5..cbaa47b006 100644 --- a/source/qre/src/trace/transforms/dynamic_memory_compute/tests.rs +++ b/source/qre/src/trace/transforms/dynamic_memory_compute/tests.rs @@ -270,7 +270,7 @@ fn block_with_single_repetition_no_restore() { // A block with repetitions=1 behaves like a flat sequence. let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); - let block = trace.add_block(1); + let block = trace.add_block(1.0); block.add_operation(H, vec![1], vec![]); block.add_operation(H, vec![2], vec![]); // evicts q0 @@ -282,7 +282,7 @@ fn block_with_single_repetition_no_restore() { // Block structure is preserved: root has one child block with reps=1. let blocks = child_blocks(&result.block); assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].repetitions, 1); + assert_eq!(blocks[0].repetitions, 1.0); let gates = collect_gates(&result); assert_eq!(gates.iter().filter(|(id, _, _)| *id == H).count(), 3); @@ -298,7 +298,7 @@ fn repeated_block_adds_restore_ops() { let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); // lazy place q0 trace.add_operation(H, vec![1], vec![]); // lazy place q1 - let block = trace.add_block(5); + let block = trace.add_block(5.0); block.add_operation(H, vec![2], vec![]); // evicts q0, lazy places q2 let transform = DynamicMemoryCompute::new(2); @@ -311,7 +311,7 @@ fn repeated_block_adds_restore_ops() { // Block structure preserved: root has one child block with reps=5. let blocks = child_blocks(&result.block); assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].repetitions, 5); + assert_eq!(blocks[0].repetitions, 5.0); // The block body contains the eviction, the H, and the restore ops. assert!(blocks[0].operations.len() >= 3); @@ -335,7 +335,7 @@ fn repeated_block_no_change_no_restore() { // operations are needed. let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); - let block = trace.add_block(10); + let block = trace.add_block(10.0); block.add_operation(H, vec![0], vec![]); // q0 already in compute let transform = DynamicMemoryCompute::new(2); @@ -346,7 +346,7 @@ fn repeated_block_no_change_no_restore() { // Block structure preserved with reps=10. let blocks = child_blocks(&result.block); assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].repetitions, 10); + assert_eq!(blocks[0].repetitions, 10.0); // Body has only the H gate (no restore ops since state unchanged). assert_eq!(blocks[0].operations.len(), 1); @@ -363,7 +363,7 @@ fn repeated_block_state_restored_for_subsequent_ops() { let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); trace.add_operation(H, vec![1], vec![]); - let block = trace.add_block(3); + let block = trace.add_block(3.0); block.add_operation(H, vec![2], vec![]); // evicts q0 inside block // After the block, q0 should be back in compute (restored). trace.add_operation(H, vec![0], vec![]); // should NOT need a read @@ -376,7 +376,7 @@ fn repeated_block_state_restored_for_subsequent_ops() { // Block structure preserved with reps=3. let blocks = child_blocks(&result.block); assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].repetitions, 3); + assert_eq!(blocks[0].repetitions, 3.0); let gates = collect_gates(&result); let h_gates = gates.iter().filter(|(id, _, _)| *id == H).count(); @@ -390,8 +390,8 @@ fn nested_repeated_blocks() { let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); trace.add_operation(H, vec![1], vec![]); - let outer = trace.add_block(2); - let inner = outer.add_block(3); + let outer = trace.add_block(2.0); + let inner = outer.add_block(3.0); inner.add_operation(H, vec![2], vec![]); // evicts q0 let transform = DynamicMemoryCompute::new(2); @@ -404,12 +404,12 @@ fn nested_repeated_blocks() { // Outer block (reps=2) preserved in root. let outer_blocks = child_blocks(&result.block); assert_eq!(outer_blocks.len(), 1); - assert_eq!(outer_blocks[0].repetitions, 2); + assert_eq!(outer_blocks[0].repetitions, 2.0); // Inner block (reps=3) preserved inside outer. let inner_blocks = child_blocks(outer_blocks[0]); assert_eq!(inner_blocks.len(), 1); - assert_eq!(inner_blocks[0].repetitions, 3); + assert_eq!(inner_blocks[0].repetitions, 3.0); let gates = collect_gates(&result); let h_count = gates.iter().filter(|(id, _, _)| *id == H).count(); @@ -431,7 +431,7 @@ fn restore_does_not_clobber_memory() { trace.add_operation(H, vec![2], vec![]); // evicts q0, places q2 lazily // Now state: slot[0]=q2, slot[1]=q1, q0 in memory - let block = trace.add_block(2); + let block = trace.add_block(2.0); block.add_operation(H, vec![0], vec![]); block.add_operation(CX, vec![0, 1], vec![]); // uses q2 and q1 (both in compute) @@ -443,7 +443,7 @@ fn restore_does_not_clobber_memory() { // Block structure preserved with reps=2. let blocks = child_blocks(&result.block); assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].repetitions, 2); + assert_eq!(blocks[0].repetitions, 2.0); // Verify it produces a valid trace with operations. let gates = collect_gates(&result); diff --git a/source/qre/src/trace/transforms/unmemory.rs b/source/qre/src/trace/transforms/unmemory.rs index 0a8f01158a..6639b19bba 100644 --- a/source/qre/src/trace/transforms/unmemory.rs +++ b/source/qre/src/trace/transforms/unmemory.rs @@ -121,7 +121,7 @@ fn process_block( Operation::BlockOperation(inner) => { let out_inner = output.add_block(inner.repetitions); - if inner.repetitions > 1 { + if inner.repetitions > 1.0 { // Repeated blocks maintain a consistent mapping across // iterations. Save the entry state and restore it after // processing so the parent context is unaffected. diff --git a/source/qre/src/trace/transforms/unmemory/tests.rs b/source/qre/src/trace/transforms/unmemory/tests.rs index 7cc100e795..0ce623eb54 100644 --- a/source/qre/src/trace/transforms/unmemory/tests.rs +++ b/source/qre/src/trace/transforms/unmemory/tests.rs @@ -170,7 +170,7 @@ fn round_trip_with_repeated_block() { let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); trace.add_operation(H, vec![1], vec![]); - let block = trace.add_block(5); + let block = trace.add_block(5.0); block.add_operation(H, vec![2], vec![]); let memorized = DynamicMemoryCompute::new(2) @@ -196,7 +196,7 @@ fn preserves_block_structure() { // Verify that block structure is preserved through round-trip. let mut trace = Trace::new(3); trace.add_operation(H, vec![0], vec![]); - let block = trace.add_block(10); + let block = trace.add_block(10.0); block.add_operation(H, vec![1], vec![]); let memorized = DynamicMemoryCompute::new(2) @@ -218,5 +218,5 @@ fn preserves_block_structure() { }) .collect(); assert_eq!(child_blocks.len(), 1); - assert_eq!(child_blocks[0].repetitions, 10); + assert_eq!(child_blocks[0].repetitions, 10.0); }