From 5ee4ec53626d4ce00574d0b4112a92ad183def56 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Mon, 6 Jul 2026 16:42:28 -0700 Subject: [PATCH 1/2] Build trace from Q# program. --- Cargo.lock | 3 + source/qdk_package/qdk/_native.pyi | 19 + .../qdk/qre/application/_openqasm.py | 2 +- .../qdk/qre/application/_qsharp.py | 10 +- source/qdk_package/qdk/qre/interop/_qsharp.py | 99 +++- source/qdk_package/src/interpreter.rs | 29 ++ source/qdk_package/src/qre.rs | 6 + .../tests/qre/test_qsharp_interop.py | 104 +++++ source/qre/Cargo.toml | 3 + source/qre/src/lib.rs | 2 + source/qre/src/trace_builder.rs | 423 ++++++++++++++++++ source/qre/src/trace_builder/tests.rs | 371 +++++++++++++++ 12 files changed, 1052 insertions(+), 19 deletions(-) create mode 100644 source/qdk_package/tests/qre/test_qsharp_interop.py create mode 100644 source/qre/src/trace_builder.rs create mode 100644 source/qre/src/trace_builder/tests.rs diff --git a/Cargo.lock b/Cargo.lock index ad1e7a1105c..a359486e74e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2132,9 +2132,12 @@ dependencies = [ name = "qre" version = "0.0.0" dependencies = [ + "indoc", "miette", "num-traits", "probability", + "qsc", + "rand 0.10.1", "rustc-hash", "serde", "thiserror", diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index 41e4be1bb3f..cd6c7bed642 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -14,6 +14,8 @@ from typing import ( overload, ) +from .qre import Trace + # pylint: disable=unused-argument # E302 is fighting with the formatter for number of blank lines # flake8: noqa: E302 @@ -321,6 +323,23 @@ class Interpreter: """ ... + def trace( + self, + entry_expr: Optional[str] = None, + callable: Optional[GlobalCallable | Closure] = None, + args: Optional[Any] = None, + ) -> Trace: + """ + Builds a QRE trace for Q# source code. + + :param entry_expr: The entry expression to run. + :param callable: The callable to run, if no entry expression is provided. + :param args: The arguments to pass to the callable, if any. + + :returns trace: The resource-estimation trace. + """ + ... + def set_quantum_seed(self, seed: Optional[int]) -> None: """ Sets the seed for the quantum random number generator. diff --git a/source/qdk_package/qdk/qre/application/_openqasm.py b/source/qdk_package/qdk/qre/application/_openqasm.py index ed75112c532..677acc6f31c 100644 --- a/source/qdk_package/qdk/qre/application/_openqasm.py +++ b/source/qdk_package/qdk/qre/application/_openqasm.py @@ -65,4 +65,4 @@ def get_trace(self, parameters: None = None) -> Trace: import_openqasm(self.program, name=name, program_type=ProgramType.File) self.program = getattr(code.qasm_import, name) - return trace_from_entry_expr(self.program, *self.args) + return trace_from_entry_expr(self.program, args=self.args) diff --git a/source/qdk_package/qdk/qre/application/_qsharp.py b/source/qdk_package/qdk/qre/application/_qsharp.py index bfc11d1a986..7c865feda67 100644 --- a/source/qdk_package/qdk/qre/application/_qsharp.py +++ b/source/qdk_package/qdk/qre/application/_qsharp.py @@ -30,6 +30,8 @@ class QSharpApplication(Application[None]): provided. Default is an empty tuple. cache_dir (Path): Directory for caching compiled traces. use_cache (bool): Whether to use the trace cache. Default is False. + use_trace_backend (bool): Whether to build traces directly from a Q# backend + instead of via logical counts. Default is False. """ entry_expr: str | Callable | LogicalCounts @@ -38,6 +40,7 @@ class QSharpApplication(Application[None]): default=Path.home() / ".cache" / "re3" / "qsharp", repr=False ) use_cache: bool = field(default=False, repr=False) + use_trace_backend: bool = field(default=False, repr=False) def __post_init__(self): """Log telemetry for QSharpApplication creation.""" @@ -57,4 +60,9 @@ def get_trace(self, parameters: None = None) -> Trace: else: cache_path = None - return trace_from_entry_expr_cached(self.entry_expr, cache_path, *self.args) + return trace_from_entry_expr_cached( + self.entry_expr, + cache_path=None if self.use_trace_backend else cache_path, + use_trace_backend=self.use_trace_backend, + args=self.args, + ) diff --git a/source/qdk_package/qdk/qre/interop/_qsharp.py b/source/qdk_package/qdk/qre/interop/_qsharp.py index 9bac4fe867a..805aafb279a 100644 --- a/source/qdk_package/qdk/qre/interop/_qsharp.py +++ b/source/qdk_package/qdk/qre/interop/_qsharp.py @@ -7,7 +7,12 @@ import time from typing import Callable, Optional -from ..._interpreter import logical_counts +from ..._interpreter import ( + Closure, + GlobalCallable, + _get_context_or_default, + logical_counts, +) from ...estimator import LogicalCounts from .._qre import Trace from ..instruction_ids import CCX, MEAS_Z, RZ, T, READ_FROM_MEMORY, WRITE_TO_MEMORY @@ -54,20 +59,11 @@ def _bucketize_rotation_counts( return result -def trace_from_entry_expr(entry_expr: str | Callable | LogicalCounts, *args) -> Trace: - """Convert a Q# entry expression into a resource-estimation Trace. - - Evaluates the entry expression to obtain logical counts, then builds - a trace containing the corresponding quantum operations. - - Args: - entry_expr (str or :class:`~typing.Callable` or :class:`~qdk.estimator.LogicalCounts`): A Q# entry expression - string, a callable, or pre-computed logical counts. - *args: The arguments to pass to the callable, if one is provided. - - Returns: - :class:`~qdk.qre.Trace`: A trace representing the resource profile of the program. - """ +def _trace_from_entry_expr_using_logical_counts( + entry_expr: str | Callable | LogicalCounts, + args: tuple, +) -> Trace: + """Build a Trace from logical counts.""" start = time.time_ns() counts = ( @@ -125,8 +121,70 @@ def trace_from_entry_expr(entry_expr: str | Callable | LogicalCounts, *args) -> return trace +def _trace_from_entry_expr_using_trace_builder( + entry_expr: str | Callable, args: tuple +) -> Trace: + """Build a Trace directly from Q# execution via the native trace backend.""" + context = _get_context_or_default(entry_expr) + + start = time.time_ns() + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + context._check_same_context_callable(entry_expr) + interpreter_args = context._python_args_to_interpreter_args(args) + trace = context._interpreter.trace( + callable=getattr(entry_expr, "__global_callable"), args=interpreter_args + ) + elif isinstance(entry_expr, (GlobalCallable, Closure)): + interpreter_args = context._python_args_to_interpreter_args(args) + trace = context._interpreter.trace(callable=entry_expr, args=interpreter_args) + else: + assert isinstance(entry_expr, str) + trace = context._interpreter.trace(entry_expr=entry_expr) + evaluation_time = time.time_ns() - start + + trace.set_property(EVALUATION_TIME, evaluation_time) + trace.set_property(ALGORITHM_COMPUTE_QUBITS, trace.compute_qubits) + trace.set_property(ALGORITHM_MEMORY_QUBITS, trace.memory_qubits or 0) + return trace + + +def trace_from_entry_expr( + entry_expr: str | Callable | LogicalCounts, + use_trace_backend: bool = False, + args: tuple = (), +) -> Trace: + """Convert a Q# entry expression into a resource-estimation Trace. + + Evaluates the entry expression to obtain logical counts, then builds + a trace containing the corresponding quantum operations. + + Args: + entry_expr (str or :class:`~typing.Callable` or :class:`~qdk.estimator.LogicalCounts`): A Q# entry expression + string, a callable, or pre-computed logical counts. + use_trace_backend (bool): If True, uses the native Q# trace backend to + build the trace directly from execution. If False, derives the + trace from logical counts. + args (tuple): Positional arguments to pass to the callable entry + expression, if one is provided. + + Returns: + :class:`~qdk.qre.Trace`: A trace representing the resource profile of the program. + """ + if use_trace_backend: + if isinstance(entry_expr, LogicalCounts): + raise TypeError( + "LogicalCounts input is not supported when use_trace_backend=True" + ) + return _trace_from_entry_expr_using_trace_builder(entry_expr, args) + else: + return _trace_from_entry_expr_using_logical_counts(entry_expr, args) + + def trace_from_entry_expr_cached( - entry_expr: str | Callable | LogicalCounts, cache_path: Optional[Path], *args + entry_expr: str | Callable | LogicalCounts, + cache_path: Optional[Path], + use_trace_backend: bool = False, + args: tuple = (), ) -> Trace: """Convert a Q# entry expression into a Trace, with optional caching. @@ -139,6 +197,11 @@ def trace_from_entry_expr_cached( string, a callable, or pre-computed logical counts. cache_path (Optional[Path]): Path for reading/writing the cached trace. If None, caching is disabled. + use_trace_backend (bool): Passed through to + ``trace_from_entry_expr``. If True, uses the native trace backend; + otherwise uses the logical-counts-based path. + args (tuple): Positional arguments to pass to the callable entry + expression, if one is provided. Returns: :class:`~qdk.qre.Trace`: A trace representing the resource profile of the program. @@ -146,7 +209,9 @@ def trace_from_entry_expr_cached( if cache_path and cache_path.exists(): return Trace.from_json(cache_path.read_text(encoding="utf-8")) - trace = trace_from_entry_expr(entry_expr, *args) + trace = trace_from_entry_expr( + entry_expr, use_trace_backend=use_trace_backend, args=args + ) if cache_path: cache_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/source/qdk_package/src/interpreter.rs b/source/qdk_package/src/interpreter.rs index 9505ab0a981..a584d227f59 100644 --- a/source/qdk_package/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -1031,6 +1031,35 @@ impl Interpreter { )), } } + + #[pyo3(signature=(entry_expr=None, callable=None, args=None))] + fn trace( + &mut self, + py: Python, + entry_expr: Option<&str>, + callable: Option>, + args: Option>, + ) -> PyResult { + let results = if let Some(entry_expr) = entry_expr { + ::qre::trace_expr(&mut self.interpreter, entry_expr) + } else { + let callable = callable.ok_or_else(|| { + QSharpError::new_err("either entry_expr or callable must be specified") + })?; + let callable = extract_callable_value(py, &callable)?; + let (input_ty, output_ty) = self + .interpreter + .global_callable_ty(&callable) + .ok_or(QSharpError::new_err("callable not found"))?; + let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?; + ::qre::trace_call(&mut self.interpreter, callable, args) + }; + + match results { + Ok(trace) => Ok(crate::qre::Trace::from_qre_trace(trace)), + Err(errors) => Err(QSharpError::new_err(format_errors(errors))), + } + } } fn args_to_values( diff --git a/source/qdk_package/src/qre.rs b/source/qdk_package/src/qre.rs index 0ef260e92f6..83db8fe6df4 100644 --- a/source/qdk_package/src/qre.rs +++ b/source/qdk_package/src/qre.rs @@ -1150,6 +1150,12 @@ impl FactoryResult { #[pyclass] pub struct Trace(qre::Trace); +impl Trace { + pub(crate) fn from_qre_trace(trace: qre::Trace) -> Self { + Self(trace) + } +} + #[pymethods] impl Trace { #[new] diff --git a/source/qdk_package/tests/qre/test_qsharp_interop.py b/source/qdk_package/tests/qre/test_qsharp_interop.py new file mode 100644 index 00000000000..b5a94b4bba5 --- /dev/null +++ b/source/qdk_package/tests/qre/test_qsharp_interop.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from qdk._context import Context +from qdk.qre import ISA, LOGICAL, PSSPC, LatticeSurgery, linear_function +from qdk.qre._qre import _ProvenanceGraph +from qdk.qre.application import QSharpApplication +from qdk.qre.instruction_ids import ( + CCX, + CX, + LATTICE_SURGERY, + MEAS_RESET_Z, + MEAS_Z, + PAULI_X, + T, +) + +SMALL_PROGRAM = """ +{ + use (a, b, c) = (Qubit(), Qubit(), Qubit()); + X(a); + CNOT(a, b); + CCNOT(a, b, c); + let _ = M(a); + MResetZ(b); +} +""" + + +def _make_basic_isa() -> ISA: + graph = _ProvenanceGraph() + return graph.make_isa( + [ + graph.add_instruction( + LATTICE_SURGERY, + encoding=LOGICAL, + arity=None, + time=1000, + space=linear_function(50), + error_rate=linear_function(1e-6), + ), + graph.add_instruction( + T, encoding=LOGICAL, time=1000, space=400, error_rate=1e-8 + ), + ] + ) + + +def test_qsharp_trace_backend_from_entry_expr(): + app = QSharpApplication(SMALL_PROGRAM, use_trace_backend=True) + trace = app.get_trace() + + assert trace.compute_qubits == 3 + assert trace.total_qubits == 3 + assert trace.num_gates == 5 + assert trace.depth == 4 + assert {c.id for c in trace.required_isa} == { + PAULI_X, + CX, + CCX, + MEAS_Z, + MEAS_RESET_Z, + } + + transformed = PSSPC(num_ts_per_rotation=16, ccx_magic_states=False).transform(trace) + assert transformed is not None + transformed = LatticeSurgery().transform(transformed) + assert transformed is not None + + result = transformed.estimate(_make_basic_isa(), max_error=float("inf")) + assert result is not None + assert result.qubits > 0 + assert result.runtime > 0 + + +def test_qsharp_trace_backend_from_callable(context: Context): + context.eval( + """ + namespace TestTraceInterop { + operation Entry() : Unit { + use (a, b, c) = (Qubit(), Qubit(), Qubit()); + X(a); + CNOT(a, b); + CCNOT(a, b, c); + let _ = M(a); + MResetZ(b); + } + } + """ + ) + + app = QSharpApplication(context.code.TestTraceInterop.Entry, use_trace_backend=True) + trace = app.get_trace() + + assert trace.compute_qubits == 3 + assert trace.num_gates == 5 + assert {c.id for c in trace.required_isa} == { + PAULI_X, + CX, + CCX, + MEAS_Z, + MEAS_RESET_Z, + } diff --git a/source/qre/Cargo.toml b/source/qre/Cargo.toml index 888424a9f94..b969baca6ee 100644 --- a/source/qre/Cargo.toml +++ b/source/qre/Cargo.toml @@ -9,15 +9,18 @@ edition.workspace = true license.workspace = true [dependencies] +qsc = { path = "../compiler/qsc" } miette = { workspace = true } num-traits = { workspace = true } rustc-hash = { workspace = true } probability = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +rand = { workspace = true } [dev-dependencies] +indoc = { workspace = true } [lints] workspace = true diff --git a/source/qre/src/lib.rs b/source/qre/src/lib.rs index 8fdab74a642..4d6673e9916 100644 --- a/source/qre/src/lib.rs +++ b/source/qre/src/lib.rs @@ -25,6 +25,8 @@ pub use trace::{ Property, Trace, TraceTransform, Unmemory, WalkIterator, estimate_parallel, estimate_with_graph, }; +mod trace_builder; +pub use trace_builder::{TraceBuilder, trace_call, trace_expr}; mod utils; pub use utils::{binom_ppf, float_from_bits, float_to_bits}; diff --git a/source/qre/src/trace_builder.rs b/source/qre/src/trace_builder.rs new file mode 100644 index 00000000000..bf1e7c688b3 --- /dev/null +++ b/source/qre/src/trace_builder.rs @@ -0,0 +1,423 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] +mod tests; + +use qsc::{ + Backend, BackendResult, + interpret::{self, GenericReceiver, Interpreter, Value}, +}; +use rand::RngExt; +use rustc_hash::FxHashMap; + +use crate::{Trace, instruction_ids}; + +#[derive(Default)] +pub struct TraceBuilder { + trace: Trace, + qubit_id_map: FxHashMap, + post_select_measurements: FxHashMap, + repeat_frames: Vec, + free_list: Vec, + next_free: usize, + live_qubits: usize, + max_live_qubits: usize, +} + +enum PendingOperation { + Gate { + id: u64, + qubits: Vec, + params: Vec, + }, + Block { + repetitions: u64, + operations: Vec, + }, +} + +#[derive(Default)] +struct RepeatFrame { + repetitions: u64, + operations: Vec, +} + +impl TraceBuilder { + #[must_use] + pub fn into_trace(mut self) -> Trace { + self.trace.set_compute_qubits(self.max_live_qubits as u64); + self.trace + } + + fn on_allocate(&mut self, q: usize) { + self.live_qubits += 1; + self.max_live_qubits = self.max_live_qubits.max(self.live_qubits); + + // Keep compute qubit count conservative while building and exact on finalize. + self.trace.set_compute_qubits(self.max_live_qubits as u64); + + // Ensure qubit indices are stable within the trace for readability. + if q >= self.next_free { + self.next_free = q + 1; + } + } + + fn map_qubit(&self, q: usize) -> u64 { + self.qubit_id_map.get(&q).copied().unwrap_or(q) as u64 + } + + fn push_operation(&mut self, op: PendingOperation) { + if let Some(frame) = self.repeat_frames.last_mut() { + frame.operations.push(op); + } else { + Self::append_operation_to_block(self.trace.root_block_mut(), op); + } + } + + fn append_operation_to_block(block: &mut crate::Block, op: PendingOperation) { + match op { + PendingOperation::Gate { id, qubits, params } => { + block.add_operation(id, qubits, params); + } + PendingOperation::Block { + repetitions, + operations, + } => { + let child = block.add_block(repetitions); + for op in operations { + Self::append_operation_to_block(child, op); + } + } + } + } + + fn push_gate(&mut self, id: u64, qubits: Vec, params: Vec) { + self.push_operation(PendingOperation::Gate { id, qubits, params }); + } + + fn load(&mut self, q: usize) { + self.push_gate( + instruction_ids::READ_FROM_MEMORY, + vec![self.map_qubit(q), self.map_qubit(q)], + vec![], + ); + } + + fn store(&mut self, q: usize) { + self.push_gate( + instruction_ids::WRITE_TO_MEMORY, + vec![self.map_qubit(q), self.map_qubit(q)], + vec![], + ); + } + + fn measurement_result(&mut self, q: usize) -> bool { + self.post_select_measurements + .remove(&q) + .unwrap_or_else(|| rand::rng().random_bool(0.5)) + } +} + +impl Backend for TraceBuilder { + fn x(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::PAULI_X, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn cx(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::CX, + vec![self.map_qubit(ctl), self.map_qubit(q)], + vec![], + ); + Ok(()) + } + + fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::CCX, + vec![ + self.map_qubit(ctl0), + self.map_qubit(ctl1), + self.map_qubit(q), + ], + vec![], + ); + Ok(()) + } + + fn m(&mut self, q: usize) -> Result { + let val = self.measurement_result(q); + self.push_gate(instruction_ids::MEAS_Z, vec![self.map_qubit(q)], vec![]); + Ok(val.into()) + } + + fn mresetz(&mut self, q: usize) -> Result { + let val = self.measurement_result(q); + self.push_gate( + instruction_ids::MEAS_RESET_Z, + vec![self.map_qubit(q)], + vec![], + ); + Ok(val.into()) + } + + fn reset(&mut self, _q: usize) -> Result<(), String> { + Ok(()) + } + + fn qubit_allocate(&mut self) -> Result { + let q = self.free_list.pop().unwrap_or(self.next_free); + self.on_allocate(q); + self.qubit_id_map.insert(q, q); + Ok(q) + } + + fn qubit_release(&mut self, q: usize) -> Result { + self.live_qubits = self.live_qubits.saturating_sub(1); + let q = self.qubit_id_map.remove(&q).unwrap_or(q); + self.free_list.push(q); + Ok(true) + } + + fn h(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::H, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn cy(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::CY, + vec![self.map_qubit(ctl), self.map_qubit(q)], + vec![], + ); + Ok(()) + } + + fn cz(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::CZ, + vec![self.map_qubit(ctl), self.map_qubit(q)], + vec![], + ); + Ok(()) + } + + fn rx(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::RX, vec![self.map_qubit(q)], vec![theta]); + Ok(()) + } + + fn rxx(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::RXX, + vec![self.map_qubit(q0), self.map_qubit(q1)], + vec![theta], + ); + Ok(()) + } + + fn ry(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::RY, vec![self.map_qubit(q)], vec![theta]); + Ok(()) + } + + fn ryy(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::RYY, + vec![self.map_qubit(q0), self.map_qubit(q1)], + vec![theta], + ); + Ok(()) + } + + fn rz(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::RZ, vec![self.map_qubit(q)], vec![theta]); + Ok(()) + } + + fn rzz(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::RZZ, + vec![self.map_qubit(q0), self.map_qubit(q1)], + vec![theta], + ); + Ok(()) + } + + fn sadj(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::S_DAG, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn s(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::S, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn sx(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::SQRT_X, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn swap(&mut self, q0: usize, q1: usize) -> Result<(), String> { + self.push_gate( + instruction_ids::SWAP, + vec![self.map_qubit(q0), self.map_qubit(q1)], + vec![], + ); + Ok(()) + } + + fn tadj(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::T_DAG, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn t(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::T, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn y(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::PAULI_Y, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn z(&mut self, q: usize) -> Result<(), String> { + self.push_gate(instruction_ids::PAULI_Z, vec![self.map_qubit(q)], vec![]); + Ok(()) + } + + fn qubit_swap_id(&mut self, q0: usize, q1: usize) -> Result<(), String> { + let q0_id = self.qubit_id_map.remove(&q0).unwrap_or(q0); + let q1_id = self.qubit_id_map.remove(&q1).unwrap_or(q1); + self.qubit_id_map.insert(q0, q1_id); + self.qubit_id_map.insert(q1, q0_id); + + let q0_post_select = self.post_select_measurements.remove(&q0); + let q1_post_select = self.post_select_measurements.remove(&q1); + if let Some(val) = q0_post_select { + self.post_select_measurements.insert(q1, val); + } + if let Some(val) = q1_post_select { + self.post_select_measurements.insert(q0, val); + } + + Ok(()) + } + + fn qubit_is_zero(&mut self, _q: usize) -> Result { + Ok(true) + } + + fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option> { + // The trace builder intentionally handles only the subset of resource-estimation + // intrinsics that can be represented directly in the current trace model. + // `BeginEstimateCaching` and `EndEstimateCaching` are treated as no-ops, `Load` + // and `Store` emit explicit memory-operation gates, `EnableMemoryComputeArchitecture` + // remains a no-op until the qubit-id mapping is implemented, and + // `AccountForEstimatesInternal` is rejected because the trace model cannot + // represent its accounting directly yet. + match name { + "BeginRepeatEstimatesInternal" => { + let count = arg.unwrap_int(); + let repetitions: u64 = match count.try_into() { + Ok(repetitions) => repetitions, + Err(_) => { + return Some(Err(format!( + "Estimate count {count} is too large to fit in a u64." + ))); + } + }; + self.repeat_frames.push(RepeatFrame { + repetitions, + operations: Vec::new(), + }); + Some(Ok(Value::unit())) + } + "EndRepeatEstimatesInternal" => { + let Some(frame) = self.repeat_frames.pop() else { + return Some(Err("cannot end repeat before beginning repeat".to_string())); + }; + + if frame.repetitions <= 1 { + for op in frame.operations { + self.push_operation(op); + } + } else { + self.push_operation(PendingOperation::Block { + repetitions: frame.repetitions, + operations: frame.operations, + }); + } + + Some(Ok(Value::unit())) + } + "BeginEstimateCaching" => Some(Ok(Value::Bool(true))), + "EndEstimateCaching" + | "GlobalPhase" + | "ConfigurePauliNoise" + | "ConfigureQubitLoss" + | "ApplyIdleNoise" + | "EnableMemoryComputeArchitecture" => Some(Ok(Value::unit())), + "Load" => { + let q = arg.unwrap_qubit().deref().0; + self.load(q); + Some(Ok(Value::unit())) + } + "Store" => { + let q = arg.unwrap_qubit().deref().0; + self.store(q); + Some(Ok(Value::unit())) + } + "AccountForEstimatesInternal" => Some(Err( + "AccountForEstimatesInternal is not supported in trace builder; the trace model cannot represent this accounting directly yet".to_string(), + )), + "PostSelectZ" => { + let values = arg.unwrap_tuple(); + let [result, qubit] = std::array::from_fn(|i| values[i].clone()); + let Value::Result(BackendResult::Val(val)) = result else { + panic!("first argument to PostSelectZ should be a measurement result"); + }; + let qubit = qubit.unwrap_qubit().deref().0; + self.post_select_measurements.insert(qubit, val); + + Some(Ok(Value::unit())) + } + _ => None, + } + } +} + +pub fn trace_expr( + interpreter: &mut Interpreter, + expr: &str, +) -> Result> { + let mut builder = TraceBuilder::default(); + let mut stdout = std::io::sink(); + let mut out = GenericReceiver::new(&mut stdout); + + interpreter + .run_with_sim(&mut builder, &mut out, Some(expr), None) + .map_err(|e| e.into_iter().collect::>())?; + + Ok(builder.into_trace()) +} + +pub fn trace_call( + interpreter: &mut Interpreter, + callable: Value, + args: Value, +) -> Result> { + let mut builder = TraceBuilder::default(); + let mut stdout = std::io::sink(); + let mut out = GenericReceiver::new(&mut stdout); + + interpreter + .invoke_with_sim(&mut builder, &mut out, callable, args, None) + .map_err(|e| e.into_iter().collect::>())?; + + Ok(builder.into_trace()) +} diff --git a/source/qre/src/trace_builder/tests.rs b/source/qre/src/trace_builder/tests.rs new file mode 100644 index 00000000000..f192a30a15a --- /dev/null +++ b/source/qre/src/trace_builder/tests.rs @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use indoc::indoc; +use miette::Report; +use qsc::{ + Backend, LanguageFeatures, PackageType, SourceMap, TargetCapabilityFlags, + interpret::{Interpreter, Value}, + target::Profile, +}; + +use crate::{ + instruction_ids::*, + trace::Gate, + trace_builder::{TraceBuilder, trace_expr}, +}; + +fn build_interpreter(source: &str) -> Result { + let source_map = SourceMap::new([("test".into(), source.into())], None); + let (std_id, store) = qsc::compile::package_store_with_stdlib(TargetCapabilityFlags::all()); + + match Interpreter::new( + source_map, + PackageType::Exe, + Profile::Unrestricted.into(), + LanguageFeatures::default(), + store, + &[(std_id, None)], + ) { + Ok(interpreter) => Ok(interpreter), + Err(err) => { + let mut messages = Vec::new(); + for e in err { + let report = Report::from(e); + messages.push(format!("{report:?}")); + } + Err(format!("compilation failed:\n{}", messages.join("\n"))) + } + } +} + +fn run_trace_result(source: &str) -> Result { + let mut interpreter = build_interpreter(source)?; + + trace_expr(&mut interpreter, "Test.Main()") + .map_err(|err| format!("evaluation failed:\n{}", format_errors(err))) +} + +fn run_trace(source: &str) -> crate::Trace { + run_trace_result(source).unwrap_or_else(|err| panic!("failed to build trace: {err}")) +} + +fn run_trace_expect_error(source: &str) -> String { + match run_trace_result(source) { + Err(err) => err, + Ok(_) => panic!("expected an error but trace succeeded"), + } +} + +fn format_errors(errors: Vec) -> String { + errors + .into_iter() + .map(|e| format!("{:?}", Report::from(e))) + .collect::>() + .join("\n") +} + +#[test] +fn supports_x_cx_ccx_and_measurements() { + let trace = run_trace(indoc! { + " + namespace Test { + @EntryPoint() + operation Main() : Unit { + use (a, b, c) = (Qubit(), Qubit(), Qubit()); + X(a); + CNOT(a, b); + CCNOT(a, b, c); + let _ = M(a); + let _ = MResetZ(b); + } + } + " + }); + + assert_eq!(trace.compute_qubits(), 3); + assert_eq!(trace.num_gates(), 5); + + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![PAULI_X, CX, CCX, MEAS_Z, MEAS_RESET_Z]); +} + +#[test] +fn supports_single_qubit_and_phase_family() { + let mut builder = TraceBuilder::default(); + let q = builder.qubit_allocate().expect("allocate should succeed"); + + builder.h(q).expect("H should be supported"); + builder.s(q).expect("S should be supported"); + builder.sadj(q).expect("SAdj should be supported"); + builder.sx(q).expect("SX should be supported"); + builder.t(q).expect("T should be supported"); + builder.tadj(q).expect("TAdj should be supported"); + builder.y(q).expect("Y should be supported"); + builder.z(q).expect("Z should be supported"); + + let trace = builder.into_trace(); + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![H, S, S_DAG, SQRT_X, T, T_DAG, PAULI_Y, PAULI_Z]); +} + +#[test] +fn supports_controlled_and_rotation_families() { + let mut builder = TraceBuilder::default(); + let q0 = builder + .qubit_allocate() + .expect("first allocate should succeed"); + let q1 = builder + .qubit_allocate() + .expect("second allocate should succeed"); + + builder.cy(q0, q1).expect("CY should be supported"); + builder.cz(q0, q1).expect("CZ should be supported"); + builder.swap(q0, q1).expect("SWAP should be supported"); + + builder.rx(0.1, q0).expect("Rx should be supported"); + builder.ry(0.2, q0).expect("Ry should be supported"); + builder.rz(0.3, q0).expect("Rz should be supported"); + builder.rxx(0.4, q0, q1).expect("Rxx should be supported"); + builder.ryy(0.5, q0, q1).expect("Ryy should be supported"); + builder.rzz(0.6, q0, q1).expect("Rzz should be supported"); + + // Label permutation should remap subsequent operations and not emit a gate. + builder + .qubit_swap_id(q0, q1) + .expect("qubit_swap_id should be supported"); + builder.x(q0).expect("X after relabel should be supported"); + builder.x(q1).expect("X after relabel should be supported"); + + let trace = builder.into_trace(); + let ops: Vec<_> = trace.walk_iter().collect(); + + let ids: Vec = ops.iter().map(|g| g.id()).collect(); + assert_eq!( + ids, + vec![CY, CZ, SWAP, RX, RY, RZ, RXX, RYY, RZZ, PAULI_X, PAULI_X] + ); + + let params: Vec> = ops.iter().map(|g| g.params().to_vec()).collect(); + assert_eq!( + params, + vec![ + vec![], + vec![], + vec![], + vec![0.1], + vec![0.2], + vec![0.3], + vec![0.4], + vec![0.5], + vec![0.6], + vec![], + vec![], + ] + ); + + // After qubit_swap_id(q0, q1), operations on q0/q1 target swapped trace qubits. + assert_eq!(ops[9].qubits(), &[q1 as u64]); + assert_eq!(ops[10].qubits(), &[q0 as u64]); +} + +#[test] +fn post_selection_one_controls_branch() { + let trace = run_trace(indoc! { + " + namespace Test { + import Std.Diagnostics.PostSelectZ; + + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + PostSelectZ(One, q); + if M(q) == One { + X(q); + } + } + } + " + }); + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![MEAS_Z, PAULI_X]); +} + +#[test] +fn post_selection_zero_controls_branch() { + let trace = run_trace(indoc! { + " + namespace Test { + import Std.Diagnostics.PostSelectZ; + + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + PostSelectZ(Zero, q); + if M(q) == One { + X(q); + } + } + } + " + }); + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![MEAS_Z]); +} + +#[test] +fn measurement_branch_is_observed_both_ways_over_multiple_runs() { + let trace = run_trace(indoc! { + " + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + for _ in 1..100 { + H(q); + if M(q) == One { + X(q); + } + } + } + } + " + }); + + let gate_counts = trace.gate_counts(); + assert_eq!(gate_counts.get(&H), Some(&100)); + assert_eq!(gate_counts.get(&MEAS_Z), Some(&100)); + + let x_count = gate_counts.get(&PAULI_X).copied().unwrap_or(0); + assert!( + (25..=75).contains(&x_count), + "expected X count to be between 25 and 75, got {x_count}" + ); +} + +#[test] +fn repeat_estimates_creates_repeated_block() { + let trace = run_trace(indoc! { + " + namespace Test { + import Std.ResourceEstimation.*; + + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + BeginRepeatEstimates(3); + X(q); + MResetZ(q); + EndRepeatEstimates(); + } + } + " + }); + + // The repeated body has two gates and is repeated 3 times. + assert_eq!(trace.num_gates(), 6); + assert_eq!(trace.depth(), 6); + + let gate_counts = trace.gate_counts(); + assert_eq!(gate_counts.get(&PAULI_X), Some(&3)); + assert_eq!(gate_counts.get(&MEAS_RESET_Z), Some(&3)); + + let rendered = trace.to_string(); + assert!( + rendered.contains("repeat 3"), + "expected rendered trace to include repeat block, got:\n{rendered}" + ); +} + +#[test] +fn estimate_caching_is_a_no_op() { + let trace = run_trace(indoc! {r#" + namespace Test { + import Std.ResourceEstimation.*; + + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + if BeginEstimateCaching("Rotate", SingleVariant()) { + X(q); + EndEstimateCaching(); + } + } + } + "#}); + + assert_eq!(trace.compute_qubits(), 1); + assert_eq!(trace.num_gates(), 1); + + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![PAULI_X]); +} + +#[test] +fn load_and_store_emit_memory_gates() { + let mut builder = TraceBuilder::default(); + let q = builder.qubit_allocate().expect("allocate should succeed"); + + builder.load(q); + builder.store(q); + + let trace = builder.into_trace(); + let ids: Vec = trace.walk_iter().map(Gate::id).collect(); + assert_eq!(ids, vec![READ_FROM_MEMORY, WRITE_TO_MEMORY]); +} + +#[test] +fn enable_memory_compute_architecture_is_a_no_op() { + let mut builder = TraceBuilder::default(); + + let result = builder + .custom_intrinsic("EnableMemoryComputeArchitecture", Value::unit()) + .expect("intrinsic should be recognized") + .expect("intrinsic should succeed"); + + assert_eq!(result, Value::unit()); + + let trace = builder.into_trace(); + assert_eq!(trace.num_gates(), 0); +} + +#[test] +fn account_for_estimates_is_rejected() { + let err = run_trace_expect_error(indoc! {r#" + namespace Test { + import Std.ResourceEstimation.*; + + @EntryPoint() + operation Main() : Unit { + use qs = Qubit[1]; + AccountForEstimates([TCount(1)], PSSPCLayout(), qs); + } + } + "#}); + + assert!( + err.contains("IntrinsicFail"), + "unexpected error message: {err}" + ); +} + +#[test] +fn end_repeat_without_begin_fails() { + let err = run_trace_expect_error(indoc! { + " + namespace Test { + import Std.ResourceEstimation.*; + + @EntryPoint() + operation Main() : Unit { + EndRepeatEstimates(); + } + } + " + }); + + assert!( + err.contains("IntrinsicFail"), + "unexpected error message: {err}" + ); +} From 043622dffcd7dbc8285e87e8139147aa1acf92d6 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 7 Jul 2026 08:56:33 -0700 Subject: [PATCH 2/2] update notebook --- samples/qre/1_qre_input.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/qre/1_qre_input.ipynb b/samples/qre/1_qre_input.ipynb index 8a39346a81b..87788d6de2b 100644 --- a/samples/qre/1_qre_input.ipynb +++ b/samples/qre/1_qre_input.ipynb @@ -349,7 +349,7 @@ " # obtain the memory/compute strategy and its name\n", " strategy, strategy_name = parameters.strategy\n", " # generate a trace from the Q# entry point with the specified parameters\n", - " trace = trace_from_entry_expr(qdk.code.Adder, self.bitsize, adder_fn, parameters.compute_fraction, strategy)\n", + " trace = trace_from_entry_expr(qdk.code.Adder, args=(self.bitsize, adder_fn, parameters.compute_fraction, strategy))\n", "\n", " # Set trace properties for later analysis and display\n", " trace.set_property(ADDER, adder_name)\n",