diff --git a/source/pip/qsharp/qre/_architecture.py b/source/pip/qsharp/qre/_architecture.py index cd8bb52e64..f2c72031af 100644 --- a/source/pip/qsharp/qre/_architecture.py +++ b/source/pip/qsharp/qre/_architecture.py @@ -92,7 +92,7 @@ def add_instruction( error_rate: float | _FloatFunction = 0.0, transform: ISATransform | None = None, source: list[Instruction] | None = None, - **kwargs: int, + **kwargs: int | float, ) -> int: """ Create an instruction and add it to the provenance graph. diff --git a/source/pip/qsharp/qre/_instruction.py b/source/pip/qsharp/qre/_instruction.py index 4669a86d4c..2c52f96e17 100644 --- a/source/pip/qsharp/qre/_instruction.py +++ b/source/pip/qsharp/qre/_instruction.py @@ -44,7 +44,7 @@ def constraint( *, arity: Optional[int] = 1, error_rate: Optional[ConstraintBound] = None, - **kwargs: bool, + **kwargs: bool | ConstraintBound, ) -> Constraint: """ Create an instruction constraint. @@ -55,8 +55,10 @@ def constraint( arity (Optional[int]): The instruction arity. If None, instruction is assumed to have variable arity. Default is 1. error_rate (Optional[ConstraintBound]): The constraint on the error rate. - **kwargs (bool): Required properties that matching instructions must have. - Valid property names: distance. Set to True to require the property. + **kwargs (bool | ConstraintBound): Required property conditions for + matching instructions. Pass ``True`` to require that the property + exists, or pass a ``ConstraintBound`` to require a numeric + property value to satisfy that bound. Returns: Constraint: The instruction constraint. @@ -67,11 +69,20 @@ def constraint( c = Constraint(id, encoding, arity, error_rate) for key, value in kwargs.items(): - if value: - if (prop_key := property_name_to_key(key)) is None: - raise ValueError(f"Unknown property '{key}'") + if not value: + continue + if (prop_key := property_name_to_key(key)) is None: + raise ValueError(f"Unknown property '{key}'") + + if value is True: c.add_property(prop_key) + elif isinstance(value, ConstraintBound): + c.add_property_bound(prop_key, value) + else: + raise TypeError( + f"Property constraint '{key}' must be a bool or ConstraintBound" + ) return c diff --git a/source/pip/qsharp/qre/_qre.pyi b/source/pip/qsharp/qre/_qre.pyi index 370bd2c886..334f3e77aa 100644 --- a/source/pip/qsharp/qre/_qre.pyi +++ b/source/pip/qsharp/qre/_qre.pyi @@ -377,17 +377,17 @@ class Instruction: """ ... - def set_property(self, key: int, value: int) -> None: + def set_property(self, key: int, value: bool | int | float | str) -> None: """ Set a property on the instruction. Args: key (int): The property key. - value (int): The property value. + value (bool | int | float | str): The property value. """ ... - def get_property(self, key: int) -> Optional[int]: + def get_property(self, key: int) -> Optional[bool | int | float | str]: """ Get a property by its key. @@ -395,7 +395,7 @@ class Instruction: key (int): The property key. Returns: - Optional[int]: The property value, or None if not found. + Optional[bool | int | float | str]: The property value, or None if not found. """ ... @@ -411,20 +411,22 @@ class Instruction: """ ... - def get_property_or(self, key: int, default: int) -> int: + def get_property_or( + self, key: int, default: bool | int | float | str + ) -> bool | int | float | str: """ Get a property by its key, or return a default value if not found. Args: key (int): The property key. - default (int): The default value to return if the property is not found. + default (bool | int | float | str): The default value to return if the property is not found. Returns: - int: The property value, or the default value if not found. + bool | int | float | str: The property value, or the default value if not found. """ ... - def __getitem__(self, key: int) -> int: + def __getitem__(self, key: int) -> bool | int | float | str: """ Get a property by its key, or raise an error if not found. @@ -432,7 +434,7 @@ class Instruction: key (int): The property key. Returns: - int: The property value. + bool | int | float | str: The property value. """ ... @@ -593,6 +595,17 @@ class Constraint: """ ... + def add_property_bound(self, property: int, bound: ConstraintBound) -> None: + """ + Add a numeric property bound requirement to the constraint. + + Args: + property (int): The property key to constrain. + bound (ConstraintBound): The numeric bound that matching instructions + must satisfy for this property. + """ + ... + def has_property(self, property: int) -> bool: """ Check if the constraint requires a specific property. diff --git a/source/pip/qsharp/qre/models/__init__.py b/source/pip/qsharp/qre/models/__init__.py index 3da76797ac..6efb79c239 100644 --- a/source/pip/qsharp/qre/models/__init__.py +++ b/source/pip/qsharp/qre/models/__init__.py @@ -4,19 +4,22 @@ from .factories import Litinski19Factory, MagicUpToClifford, RoundBasedFactory from .qec import ( SurfaceCode, + SurfaceCodeLowMove, ThreeAux, OneDimensionalYokedSurfaceCode, TwoDimensionalYokedSurfaceCode, ) -from .qubits import GateBased, Majorana +from .qubits import GateBased, Majorana, NeutralAtom __all__ = [ "GateBased", "Litinski19Factory", "Majorana", "MagicUpToClifford", + "NeutralAtom", "RoundBasedFactory", "SurfaceCode", + "SurfaceCodeLowMove", "ThreeAux", "OneDimensionalYokedSurfaceCode", "TwoDimensionalYokedSurfaceCode", diff --git a/source/pip/qsharp/qre/models/qec/__init__.py b/source/pip/qsharp/qre/models/qec/__init__.py index 4e4cf816f7..2cf54a05c6 100644 --- a/source/pip/qsharp/qre/models/qec/__init__.py +++ b/source/pip/qsharp/qre/models/qec/__init__.py @@ -2,11 +2,13 @@ # Licensed under the MIT License. from ._surface_code import SurfaceCode +from ._surface_code_low_move import SurfaceCodeLowMove from ._three_aux import ThreeAux from ._yoked import OneDimensionalYokedSurfaceCode, TwoDimensionalYokedSurfaceCode __all__ = [ "SurfaceCode", + "SurfaceCodeLowMove", "ThreeAux", "OneDimensionalYokedSurfaceCode", "TwoDimensionalYokedSurfaceCode", diff --git a/source/pip/qsharp/qre/models/qec/_surface_code_low_move.py b/source/pip/qsharp/qre/models/qec/_surface_code_low_move.py new file mode 100644 index 0000000000..0880613cc4 --- /dev/null +++ b/source/pip/qsharp/qre/models/qec/_surface_code_low_move.py @@ -0,0 +1,216 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations +from dataclasses import KW_ONLY, dataclass, field +import math +from typing import Generator, Optional +from ..._instruction import ( + ISA, + ISARequirements, + ISATransform, + constraint, + ConstraintBound, + LOGICAL, +) +from ..._isa_enumeration import ISAContext +from ..._qre import linear_function +from ...instruction_ids import ( + CZ, + LATTICE_SURGERY, + MEAS_RESET_Z, + MEAS_Z, + PHYSICAL_MOVE, + RZ, + SQRT_X, +) +from ...property_keys import ( + ATOM_SPACING, + VELOCITY, + ACCELERATION, +) + + +@dataclass +class SurfaceCodeLowMove(ISATransform): + """ + This class models a rotated surface code tailored to a reconfigurable, + zoned neutral-atom architecture with mobile ancillas. + + The syndrome-extraction schedule is based on a mobile-ancilla surface-code + scheme in which a single ancilla visits the data qubits of each plaquette, + combined with the atom-transport model used by ``NeutralAtom``. In this + model, the ancilla is moved within the Rydberg interaction range of each + data atom to execute the entangling sequence, while other atoms and gate + sites remain separated by about 10 microns to suppress crosstalk. The time + model therefore combines the single-ancilla plaquette circuit with explicit + motion overhead from horizontal and diagonal transport segments. + + Attributes: + crossing_prefactor: float + The prefactor for logical error rate due to error correction + crossings. (Default is 0.03, see Eq. (11) in + [arXiv:1208.0928](https://arxiv.org/abs/1208.0928)) + error_correction_threshold: float + The error correction threshold for the surface code. (Default is + 0.01 (1%), see [arXiv:1009.3686](https://arxiv.org/abs/1009.3686)) + code_cycle_override: Optional[int] + If provided, this value will be used as the time for each syndrome + extraction cycle instead of the default calculation based on gate + times and transport overhead. (Default is None) + code_cycle_offset: int + An additional time offset to add to the syndrome extraction cycle + time. (Default is 0) + + Hyper parameters: + distance: int + The code distance of the surface code. + + References: + + - D. S. Wang, A. G. Fowler, L. C. L. Hollenberg: Quantum computing with + nearest neighbor interactions and error rates over 1%, + [arXiv:1009.3686](https://arxiv.org/abs/1009.3686) + - D. Horsman, A. G. Fowler, S. Devitt, R. Van Meter: Surface code quantum + computing by lattice surgery, + [arXiv:1111.4022](https://arxiv.org/abs/1111.4022) + - A. G. Fowler, M. Mariantoni, J. M. Martinis, A. N. Cleland: Surface + codes: Towards practical large-scale quantum computation, + [arXiv:1208.0928](https://arxiv.org/abs/1208.0928) + - D. Bluvstein, H. Levine, G. Semeghini, et al.: A quantum processor based + on coherent transport of entangled atom arrays, + [arXiv:2112.03923](https://arxiv.org/abs/2112.03923) + - D. Bluvstein, S. J. Evered, A. A. Geim, et al.: Logical quantum + processor based on reconfigurable atom arrays, + [arXiv:2312.03982](https://arxiv.org/abs/2312.03982) + - S. Jandura, L. Pecorari, G. Pupillo: Surface Code Stabilizer + Measurements for Rydberg Atoms, + [arXiv:2405.16621](https://arxiv.org/abs/2405.16621) + - W.-H. Lin, D. B. Tan, J. Cong: Reuse-Aware Compilation for Zoned Quantum + Architectures Based on Neutral Atoms, + [arXiv:2411.11784](https://arxiv.org/abs/2411.11784) + - D. Bluvstein, A. A. Geim, S. H. Li, et al.: Architectural mechanisms of + a universal fault-tolerant quantum computer, + [arXiv:2506.20661](https://arxiv.org/abs/2506.20661) + """ + + crossing_prefactor: float = 0.03 + error_correction_threshold: float = 0.01 + code_cycle_override: Optional[int] = None + code_cycle_offset: int = 0 + _: KW_ONLY + distance: int = field(default=3, metadata={"domain": range(3, 26, 2)}) + + @staticmethod + def required_isa() -> ISARequirements: + return ISARequirements( + constraint(RZ, error_rate=ConstraintBound.lt(0.01)), + constraint(SQRT_X, error_rate=ConstraintBound.lt(0.01)), + constraint(CZ, arity=2, error_rate=ConstraintBound.lt(0.01)), + constraint(MEAS_Z, error_rate=ConstraintBound.lt(0.01)), + constraint(MEAS_RESET_Z, error_rate=ConstraintBound.lt(0.01)), + constraint( + PHYSICAL_MOVE, + error_rate=ConstraintBound.lt(0.01), + atom_spacing=ConstraintBound.gt(9.9), + ), + ) + + def provided_isa( + self, impl_isa: ISA, ctx: ISAContext + ) -> Generator[ISA, None, None]: + cz = impl_isa[CZ] + rz = impl_isa[RZ] + sqrt_x = impl_isa[SQRT_X] + reset = impl_isa[MEAS_RESET_Z] + meas_z = impl_isa[MEAS_Z] + + move = impl_isa[PHYSICAL_MOVE] + atom_spacing_prop = move.get_property_or(ATOM_SPACING, 10.0) + max_vel_prop = move.get_property_or(VELOCITY, 0.25) + max_accel_prop = move.get_property_or(ACCELERATION, 5000.0) + assert isinstance(atom_spacing_prop, (int, float)) + assert isinstance(max_vel_prop, (int, float)) + assert isinstance(max_accel_prop, (int, float)) + atom_spacing = float(atom_spacing_prop) * 1e-6 # Convert from microns to meters + max_vel = float(max_vel_prop) + max_accel = float(max_accel_prop) + if atom_spacing < max_vel**2 / max_accel: + hor_seg_time = math.sqrt(atom_spacing / max_accel) + else: + extra_distance = atom_spacing - max_vel**2 / max_accel + hor_seg_time = max_vel / max_accel + extra_distance / max_vel + if math.sqrt(2) * atom_spacing < max_vel**2 / max_accel: + diag_seg_time = math.sqrt(math.sqrt(2) * atom_spacing / max_accel) + else: + extra_distance = math.sqrt(2) * atom_spacing - max_vel**2 / max_accel + diag_seg_time = max_vel / max_accel + extra_distance / max_vel + move_time = 3 * move.expect_time() + 1e9 * (2 * hor_seg_time + diag_seg_time) + + four_cz_time = math.ceil(4 * cz.expect_time() + move_time) + h_time = sqrt_x.expect_time() + 2 * rz.expect_time() + meas_time = meas_z.expect_time() + reset_time = reset.expect_time() + + physical_error_rate = max( + rz.expect_error_rate(), + cz.expect_error_rate(), + sqrt_x.expect_error_rate(), + reset.expect_error_rate(), + meas_z.expect_error_rate(), + ) + + # There are d^2 data qubits and (d^2 - 1) ancilla qubits in the rotated + # surface code. (See Section 7.1 in arXiv:1111.4022) + # Unchanged from the original SurfaceCode. + space_formula = linear_function(2 * self.distance**2 - 1) + + # Each standard syndrome extraction cycle consists of ancilla preparation, 4 + # rounds of CNOTs, and measurement. (See Fig. 2 in arXiv:1009.3686). + # But this must be modified to acount for the fact that the CNOTs are + # implemented as CZ+sqrt(X). The syndrome extraction cycle + # is repeated d times for a distance-d code. + if self.code_cycle_override is not None: + code_cycle_time = self.code_cycle_override + self.code_cycle_offset + else: + if reset_time > four_cz_time: + code_cycle_time = ( + max(reset_time, h_time) + + (self.distance + 1) + * (reset_time + h_time + self.code_cycle_offset) + + meas_time + ) + else: + code_cycle_time = ( + max(reset_time, h_time) + + (self.distance + 1) + * (four_cz_time + h_time + self.code_cycle_offset) + + meas_time + ) + time_value = code_cycle_time * self.distance + + # See Eqs. (10) and (11) in arXiv:1208.0928 + error_formula = linear_function( + self.crossing_prefactor + * ( + (physical_error_rate / self.error_correction_threshold) + ** ((self.distance + 1) // 2) + ) + ) + + # We provide a generic lattice surgery instruction (See Section 3 in + # arXiv:1111.4022) + yield ctx.make_isa( + ctx.add_instruction( + LATTICE_SURGERY, + encoding=LOGICAL, + arity=None, + space=space_formula, + time=time_value, + error_rate=error_formula, + transform=self, + source=[cz, rz, sqrt_x, reset, meas_z, move], + distance=self.distance, + code_cycle_time=code_cycle_time, + ), + ) diff --git a/source/pip/qsharp/qre/models/qubits/__init__.py b/source/pip/qsharp/qre/models/qubits/__init__.py index ab7887faf3..b030a673c9 100644 --- a/source/pip/qsharp/qre/models/qubits/__init__.py +++ b/source/pip/qsharp/qre/models/qubits/__init__.py @@ -3,5 +3,6 @@ from ._gate_based import GateBased from ._msft import Majorana +from ._neutral_atoms import NeutralAtom -__all__ = ["GateBased", "Majorana"] +__all__ = ["GateBased", "Majorana", "NeutralAtom"] diff --git a/source/pip/qsharp/qre/models/qubits/_neutral_atoms.py b/source/pip/qsharp/qre/models/qubits/_neutral_atoms.py new file mode 100644 index 0000000000..c6723e3d4b --- /dev/null +++ b/source/pip/qsharp/qre/models/qubits/_neutral_atoms.py @@ -0,0 +1,178 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from dataclasses import KW_ONLY, dataclass, field + +from ..._architecture import Architecture, ISAContext +from ..._instruction import ISA, Encoding +from ...instruction_ids import ( + CZ, + MEAS_RESET_Z, + MEAS_Z, + PHYSICAL_MOVE, + RZ, + SQRT_X, + H, + CNOT, + T, +) +from ...property_keys import ACCELERATION, ATOM_SPACING, VELOCITY + + +@dataclass +class NeutralAtom(Architecture): + """ + A movement-aware neutral-atom architecture with explicit atom transport. + + This model captures a neutral-atom device with native single-qubit + operations, Rydberg-mediated entangling gates, Z-basis measurement, and a + physical move instruction that carries hardware motion constraints. The + instruction set includes free virtual ``RZ`` rotations, single-qubit + ``SQRT_X`` and ``H`` gates, ``CZ`` as the native two-qubit interaction, + ``CNOT`` with a duration derived from one Rydberg interaction plus two + single-qubit operations, and ``MEAS_Z``/``MEAS_RESET_Z`` for readout. + + The motion model is exposed through ``PHYSICAL_MOVE`` and parameterized by + atom spacing, maximum velocity, maximum acceleration, and an optional + handoff time used when atoms enter or leave an interaction or measurement + zone. + + Args: + rydberg_time: The time (in ns) for native Rydberg-mediated two-qubit + interactions. + rydberg_error: The error rate for native two-qubit interactions. + one_qubit_time: The time (in ns) for one-qubit physical gates such as + ``SQRT_X`` and ``H``. + one_qubit_error: The error rate for one-qubit physical gates. + measurement_time: The time (in ns) for ``MEAS_Z`` and + ``MEAS_RESET_Z`` operations. + measurement_error: The error rate for measurement and measurement-reset + operations. + handoff_time: The time (in ns) for each handoff at the boundary of a + move operation. The ``PHYSICAL_MOVE`` instruction duration is + modeled as twice this value. + atom_spacing: The nominal spacing (in microns) between atoms during + transport or placement (based on atoms being in storage). + max_velocity: The maximum atom transport velocity (in m/s). + max_acceleration: The maximum atom transport acceleration (in m/s^2). + + References: + + - M. Saffman, T. G. Walker, K. Molmer: Quantum information with Rydberg + atoms, + [arXiv:0909.4777](https://arxiv.org/abs/0909.4777) + - H. Bernien, S. Schwartz, A. Keesling, et al.: Probing many-body + dynamics on a 51-atom quantum simulator, + [arXiv:1707.04344](https://arxiv.org/abs/1707.04344) + - D. Bluvstein, H. Levine, G. Semeghini, et al.: A quantum processor + based on coherent transport of entangled atom arrays, + [arXiv:2112.03923](https://arxiv.org/abs/2112.03923) + - W. Tian, W. J. Wee, A. Qu, et al.: Parallel assembly of arbitrary + defect-free atom arrays with a multi-tweezer algorithm, + [arXiv:2209.08038](https://arxiv.org/abs/2209.08038) + - S. J. Evered, D. Bluvstein, M. Kalinowski, et al.: High-fidelity + parallel entangling gates on a neutral atom quantum computer, + [arXiv:2304.05420](https://arxiv.org/abs/2304.05420) + - K. Wintersperger, F. Dommert, T. Ehmer, et al.: Neutral atom quantum + computing hardware: performance and end-user perspective, + [arXiv:2304.14360](https://arxiv.org/abs/2304.14360) + - H. Wang, P. Liu, D. B. Tan, et al.: Atomique: A Quantum Compiler for + Reconfigurable Neutral Atom Arrays, + [arXiv:2311.15123](https://arxiv.org/abs/2311.15123) + - D. Bluvstein, S. J. Evered, A. A. Geim, et al.: Logical quantum + processor based on reconfigurable atom arrays, + [arXiv:2312.03982](https://arxiv.org/abs/2312.03982) + - W.-H. Lin, D. B. Tan, J. Cong: Reuse-Aware Compilation for Zoned + Quantum Architectures Based on Neutral Atoms, + [arXiv:2411.11784](https://arxiv.org/abs/2411.11784) + - O. Savola, A. Paler: ATLAS: Efficient Atom Rearrangement for + Defect-Free Neutral-Atom Quantum Arrays Under Transport Loss, + [arXiv:2511.16303](https://arxiv.org/abs/2511.16303) + """ + + _: KW_ONLY + rydberg_time: int = field(default=500) # In units of ns. + rydberg_error: float = field(default=1e-3) + one_qubit_time: int = field(default=1000) # In units of ns. + one_qubit_error: float = field(default=1e-4) + measurement_time: int = field(default=10000) # In units of ns. + measurement_error: float = field(default=1e-4) + handoff_time: int = field(default=0) # In units of ns. + # These transport defaults are optimistic representative values and are + # not intended to model any specific neutral-atom platform. + atom_spacing: float = field(default=3.0) # In units of microns. + max_velocity: float = field(default=0.25) # In units m/s. + max_acceleration: float = field(default=5000.0) # In units m/s^2. + + def provided_isa(self, ctx: ISAContext) -> ISA: + return ctx.make_isa( + ctx.add_instruction( + RZ, + encoding=Encoding.PHYSICAL, + arity=1, + time=0, + error_rate=0.0, + ), + ctx.add_instruction( + T, + encoding=Encoding.PHYSICAL, + arity=1, + time=0, + error_rate=0.00001, + ), + ctx.add_instruction( + SQRT_X, + encoding=Encoding.PHYSICAL, + arity=1, + time=self.one_qubit_time, + error_rate=self.one_qubit_error, + ), + ctx.add_instruction( + H, + encoding=Encoding.PHYSICAL, + arity=1, + time=self.one_qubit_time, + error_rate=self.one_qubit_error, + ), + ctx.add_instruction( + CZ, + encoding=Encoding.PHYSICAL, + arity=2, + time=self.rydberg_time, + error_rate=self.rydberg_error, + ), + ctx.add_instruction( + CNOT, + encoding=Encoding.PHYSICAL, + arity=2, + time=self.rydberg_time + 2 * self.one_qubit_time, + error_rate=self.rydberg_error, + ), + ctx.add_instruction( + MEAS_Z, + encoding=Encoding.PHYSICAL, + arity=1, + time=self.measurement_time, + error_rate=self.measurement_error, + ), + ctx.add_instruction( + MEAS_RESET_Z, + encoding=Encoding.PHYSICAL, + arity=1, + time=self.measurement_time, + error_rate=self.measurement_error, + ), + ctx.add_instruction( + PHYSICAL_MOVE, + encoding=Encoding.PHYSICAL, + arity=1, + time=2 * self.handoff_time, + error_rate=1e-4, + acceleration=self.max_acceleration, + atom_spacing=self.atom_spacing, + velocity=self.max_velocity, + ), + ) + + +__all__ = ["NeutralAtom"] diff --git a/source/pip/qsharp/qre/property_keys.pyi b/source/pip/qsharp/qre/property_keys.pyi index f4a097f3f7..04ed5b9e18 100644 --- a/source/pip/qsharp/qre/property_keys.pyi +++ b/source/pip/qsharp/qre/property_keys.pyi @@ -5,6 +5,8 @@ DISTANCE: int SURFACE_CODE_ONE_QUBIT_TIME_FACTOR: int SURFACE_CODE_TWO_QUBIT_TIME_FACTOR: int ACCELERATION: int +ATOM_SPACING: int +VELOCITY: int NUM_TS_PER_ROTATION: int EXPECTED_SHOTS: int RUNTIME_SINGLE_SHOT: int diff --git a/source/pip/src/qre.rs b/source/pip/src/qre.rs index 0b7b1ef60f..33920787e9 100644 --- a/source/pip/src/qre.rs +++ b/source/pip/src/qre.rs @@ -313,26 +313,42 @@ impl Instruction { self.0.source() } - pub fn set_property(&mut self, key: u64, value: u64) { - self.0.set_property(key, value); + pub fn set_property(&mut self, key: u64, value: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.set_property(key, py_property_to_qre(value)?); + Ok(()) } - pub fn get_property(&self, key: u64) -> Option { - self.0.get_property(&key) + #[allow(clippy::needless_pass_by_value)] + pub fn get_property(self_: PyRef<'_, Self>, key: u64) -> PyResult>> { + self_ + .0 + .get_property(&key) + .map(|value| qre_property_to_py(self_.py(), value)) + .transpose() } pub fn has_property(&self, key: u64) -> bool { self.0.has_property(&key) } + #[allow(clippy::needless_pass_by_value)] #[pyo3(signature = (key, default))] - pub fn get_property_or(&self, key: u64, default: u64) -> u64 { - self.0.get_property_or(&key, default) + pub fn get_property_or<'py>( + self_: PyRef<'py, Self>, + key: u64, + default: &Bound<'py, PyAny>, + ) -> PyResult> { + if let Some(value) = self_.0.get_property(&key) { + qre_property_to_py(self_.py(), value) + } else { + Ok(default.clone()) + } } - pub fn __getitem__(&self, key: u64) -> PyResult { - match self.0.get_property(&key) { - Some(value) => Ok(value), + #[allow(clippy::needless_pass_by_value)] + pub fn __getitem__(self_: PyRef<'_, Self>, key: u64) -> PyResult> { + match self_.0.get_property(&key) { + Some(value) => qre_property_to_py(self_.py(), value), None => Err(PyKeyError::new_err(format!( "Property with key {key} not found" ))), @@ -432,6 +448,10 @@ impl Constraint { self.0.add_property(property); } + pub fn add_property_bound(&mut self, property: u64, bound: &ConstraintBound) { + self.0.add_property_bound(property, bound.0); + } + pub fn has_property(&self, property: u64) -> bool { self.0.has_property(&property) } @@ -445,6 +465,27 @@ fn convert_encoding(encoding: u64) -> PyResult { } } +fn py_property_to_qre(value: &Bound<'_, PyAny>) -> PyResult { + if value.is_instance_of::() { + Ok(qre::Property::new_bool(value.extract()?)) + } else if let Ok(i) = value.extract::() { + Ok(qre::Property::new_int(i)) + } else if let Ok(f) = value.extract::() { + Ok(qre::Property::new_float(f)) + } else { + Ok(qre::Property::new_str(value.to_string())) + } +} + +fn qre_property_to_py<'py>(py: Python<'py>, value: &qre::Property) -> PyResult> { + match value { + qre::Property::Bool(b) => PyBool::new(py, *b).into_bound_py_any(py), + qre::Property::Int(i) => PyInt::new(py, *i).into_bound_py_any(py), + qre::Property::Float(f) => PyFloat::new(py, *f).into_bound_py_any(py), + qre::Property::Str(s) => PyString::new(py, s).into_bound_py_any(py), + } +} + /// Build a `qre::Instruction` from either an existing `Instruction` Python /// object or from keyword arguments (id + encoding + arity + …). #[allow(clippy::too_many_arguments)] @@ -508,7 +549,7 @@ fn build_instruction( qre::property_name_to_key(&key_str.to_ascii_uppercase()).ok_or_else(|| { PyValueError::new_err(format!("Unknown property name: {key_str}")) })?; - let prop_value: u64 = value.extract()?; + let prop_value = py_property_to_qre(&value)?; instr.set_property(prop_key, prop_value); } } @@ -1618,6 +1659,8 @@ fn add_property_keys(m: &Bound<'_, PyModule>) -> PyResult<()> { SURFACE_CODE_ONE_QUBIT_TIME_FACTOR, SURFACE_CODE_TWO_QUBIT_TIME_FACTOR, ACCELERATION, + ATOM_SPACING, + VELOCITY, NUM_TS_PER_ROTATION, EXPECTED_SHOTS, RUNTIME_SINGLE_SHOT, diff --git a/source/pip/tests/qre/test_interop.py b/source/pip/tests/qre/test_interop.py index 99416d6bb7..e1b33ddac4 100644 --- a/source/pip/tests/qre/test_interop.py +++ b/source/pip/tests/qre/test_interop.py @@ -237,8 +237,7 @@ def test_qsharp_from_string(): def test_qsharp_from_callable(): - qsharp.eval( - """ + qsharp.eval(""" operation Test(numTs: Int) : Unit {{ use (a, b, c) = (Qubit(), Qubit(), Qubit()); for i in 1..numTs {{ @@ -247,8 +246,7 @@ def test_qsharp_from_callable(): CCNOT(a, b, c); Rz(1.2345, a); }} - """ - ) + """) for num_ts in range(1, 6): app = QSharpApplication(qsharp.code.Test, args=(num_ts,)) # type: ignore diff --git a/source/pip/tests/qre/test_isa.py b/source/pip/tests/qre/test_isa.py index e8fda19f29..c4ebd1eadf 100644 --- a/source/pip/tests/qre/test_isa.py +++ b/source/pip/tests/qre/test_isa.py @@ -5,6 +5,7 @@ from qsharp.qre import ( LOGICAL, + ConstraintBound, ISARequirements, constraint, generic_function, @@ -15,7 +16,7 @@ from qsharp.qre.models import SurfaceCode, GateBased from qsharp.qre._architecture import _make_instruction from qsharp.qre.instruction_ids import CCX, CCZ, LATTICE_SURGERY, T -from qsharp.qre.property_keys import DISTANCE +from qsharp.qre.property_keys import ACCELERATION, ATOM_SPACING, DISTANCE, VELOCITY def test_isa(): @@ -124,10 +125,44 @@ def test_instruction_constraints(): assert isa_with_dist.satisfies(reqs_no_prop) is True assert isa_with_dist.satisfies(reqs_with_prop) is True + # Test ISA.satisfies with numeric property bounds + isa_with_small_spacing = graph.make_isa( + [ + graph.add_instruction( + T, + encoding=LOGICAL, + time=1000, + error_rate=1e-8, + atom_spacing=9.5, + ), + ] + ) + isa_with_large_spacing = graph.make_isa( + [ + graph.add_instruction( + T, + encoding=LOGICAL, + time=1000, + error_rate=1e-8, + atom_spacing=10.0, + ), + ] + ) + + reqs_with_spacing_bound = ISARequirements( + constraint(T, encoding=LOGICAL, atom_spacing=ConstraintBound.gt(9.9)) + ) + + assert isa_with_small_spacing.satisfies(reqs_with_spacing_bound) is False + assert isa_with_large_spacing.satisfies(reqs_with_spacing_bound) is True + def test_property_names(): """Test property name lookup and case-insensitive key resolution.""" assert property_name(DISTANCE) == "DISTANCE" + assert property_name(ACCELERATION) == "ACCELERATION" + assert property_name(ATOM_SPACING) == "ATOM_SPACING" + assert property_name(VELOCITY) == "VELOCITY" # An unregistered property UNKNOWN = 10_000 @@ -139,9 +174,15 @@ def test_property_names(): assert property_name(UNKNOWN) == "DISTANCE" assert property_name_to_key("DISTANCE") == DISTANCE + assert property_name_to_key("ACCELERATION") == ACCELERATION + assert property_name_to_key("ATOM_SPACING") == ATOM_SPACING + assert property_name_to_key("VELOCITY") == VELOCITY # But we also allow case-insensitive lookup assert property_name_to_key("distance") == DISTANCE + assert property_name_to_key("acceleration") == ACCELERATION + assert property_name_to_key("atom_spacing") == ATOM_SPACING + assert property_name_to_key("velocity") == VELOCITY def test_block_linear_function(): diff --git a/source/pip/tests/qre/test_models.py b/source/pip/tests/qre/test_models.py index 46b236afb0..dd1c6f52c8 100644 --- a/source/pip/tests/qre/test_models.py +++ b/source/pip/tests/qre/test_models.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import pytest + from qsharp.qre import LOGICAL, PHYSICAL from qsharp.qre.instruction_ids import ( T, @@ -11,6 +13,7 @@ CZ, H, MEAS_Z, + PHYSICAL_MOVE, MEAS_X, MEAS_XX, MEAS_ZZ, @@ -19,6 +22,9 @@ PREP_Z, LATTICE_SURGERY, MEMORY, + MEAS_RESET_Z, + RZ, + SQRT_X, SQRT_SQRT_X, SQRT_SQRT_X_DAG, SQRT_SQRT_Y, @@ -29,15 +35,22 @@ from qsharp.qre.models import ( GateBased, Majorana, + NeutralAtom, RoundBasedFactory, MagicUpToClifford, Litinski19Factory, SurfaceCode, + SurfaceCodeLowMove, ThreeAux, TwoDimensionalYokedSurfaceCode, ) -from qsharp.qre.property_keys import DISTANCE - +from qsharp.qre.property_keys import ( + ACCELERATION, + ATOM_SPACING, + CODE_CYCLE_TIME, + DISTANCE, + VELOCITY, +) # --------------------------------------------------------------------------- # GateBased architecture tests @@ -178,6 +191,68 @@ def test_two_qubit_measurement_arities(self): assert isa[MEAS_ZZ].arity == 2 +# --------------------------------------------------------------------------- +# NeutralAtom architecture tests +# --------------------------------------------------------------------------- + + +class TestNeutralAtom: + def test_default_parameters(self): + arch = NeutralAtom() + + assert arch.rydberg_time == 500 + assert arch.rydberg_error == 1e-3 + assert arch.one_qubit_time == 1000 + assert arch.one_qubit_error == 1e-4 + assert arch.measurement_time == 10000 + assert arch.measurement_error == 1e-4 + assert arch.handoff_time == 0 + assert arch.atom_spacing == 3.0 + assert arch.max_velocity == 0.25 + assert arch.max_acceleration == 5000.0 + + def test_provided_isa_contains_expected_instructions(self): + arch = NeutralAtom() + isa = arch.context().isa + + for instr_id in [ + RZ, + T, + SQRT_X, + H, + CZ, + CNOT, + MEAS_Z, + MEAS_RESET_Z, + PHYSICAL_MOVE, + ]: + assert instr_id in isa + + def test_rz_is_free(self): + arch = NeutralAtom() + isa = arch.context().isa + + assert isa[RZ].expect_time() == 0 + assert isa[RZ].expect_error_rate() == 0.0 + + def test_physical_move_has_motion_properties(self): + arch = NeutralAtom( + atom_spacing=8.5, max_velocity=17.25, max_acceleration=9000.5 + ) + isa = arch.context().isa + move = isa[PHYSICAL_MOVE] + + assert move.get_property(ATOM_SPACING) == 8.5 + assert move.get_property(VELOCITY) == 17.25 + assert move.get_property(ACCELERATION) == 9000.5 + + def test_physical_move_has_expected_time(self): + arch = NeutralAtom(atom_spacing=8, handoff_time=17) + isa = arch.context().isa + + assert isa[PHYSICAL_MOVE].expect_time() == 34 + + # --------------------------------------------------------------------------- # SurfaceCode QEC tests # --------------------------------------------------------------------------- @@ -302,6 +377,79 @@ def test_custom_error_correction_threshold(self): assert error_low > error_high +class TestSurfaceCodeLowMove: + def test_removed_gate_depth_keywords_are_rejected(self): + one_qubit_kwargs = {"distance": 3, "one_qubit_gate_depth": 1} + with pytest.raises(TypeError): + SurfaceCodeLowMove(**one_qubit_kwargs) + + two_qubit_kwargs = {"distance": 3, "two_qubit_gate_depth": 4} + with pytest.raises(TypeError): + SurfaceCodeLowMove(**two_qubit_kwargs) + + def test_required_isa_is_satisfied_by_neutral_atom(self): + arch = NeutralAtom(atom_spacing=10.0) + + assert arch.context().isa.satisfies(SurfaceCodeLowMove.required_isa()) + + def test_required_isa_rejects_small_atom_spacing(self): + arch = NeutralAtom(atom_spacing=9.9) + + assert arch.context().isa.satisfies(SurfaceCodeLowMove.required_isa()) is False + + def test_uses_move_atom_spacing_property(self): + small_spacing_arch = NeutralAtom( + atom_spacing=10.0, + max_velocity=1, + max_acceleration=1, + measurement_time=1, + ) + large_spacing_arch = NeutralAtom( + atom_spacing=20.0, + max_velocity=1, + max_acceleration=1, + measurement_time=1, + ) + + small_spacing_ctx = small_spacing_arch.context() + large_spacing_ctx = large_spacing_arch.context() + sc = SurfaceCodeLowMove(distance=3) + + small_spacing_lattice_surgery = list( + sc.provided_isa(small_spacing_ctx.isa, small_spacing_ctx) + )[0][LATTICE_SURGERY] + large_spacing_lattice_surgery = list( + sc.provided_isa(large_spacing_ctx.isa, large_spacing_ctx) + )[0][LATTICE_SURGERY] + + assert small_spacing_lattice_surgery.get_property(CODE_CYCLE_TIME) != ( + large_spacing_lattice_surgery.get_property(CODE_CYCLE_TIME) + ) + + def test_provides_lattice_surgery(self): + arch = NeutralAtom(atom_spacing=10.0) + ctx = arch.context() + sc = SurfaceCodeLowMove(distance=3) + + isas = list(sc.provided_isa(ctx.isa, ctx)) + + assert len(isas) == 1 + assert LATTICE_SURGERY in isas[0] + assert isas[0][LATTICE_SURGERY].encoding == LOGICAL + + def test_time_scales_from_code_cycle_time(self): + arch = NeutralAtom(atom_spacing=10.0) + ctx = arch.context() + sc = SurfaceCodeLowMove(distance=3) + + lattice_surgery = list(sc.provided_isa(ctx.isa, ctx))[0][LATTICE_SURGERY] + code_cycle_time = lattice_surgery.get_property(CODE_CYCLE_TIME) + assert isinstance(code_cycle_time, (int, float)) + + assert code_cycle_time > 0 + assert lattice_surgery.expect_time(1) == code_cycle_time * sc.distance + + # --------------------------------------------------------------------------- # ThreeAux QEC tests # --------------------------------------------------------------------------- diff --git a/source/qre/src/isa.rs b/source/qre/src/isa.rs index cdeac80524..ff0e3c5ef4 100644 --- a/source/qre/src/isa.rs +++ b/source/qre/src/isa.rs @@ -8,11 +8,11 @@ use std::{ sync::{Arc, RwLock, RwLockReadGuard}, }; -use num_traits::FromPrimitive; +use num_traits::{FromPrimitive, ToPrimitive}; use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; -use crate::trace::instruction_ids::instruction_name; +use crate::trace::{Property, instruction_ids::instruction_name}; pub mod property_keys; @@ -267,7 +267,7 @@ pub struct Instruction { encoding: Encoding, metrics: Metrics, source: usize, - properties: Option>, + properties: Option>, } impl Instruction { @@ -418,7 +418,7 @@ impl Instruction { self.source } - pub fn set_property(&mut self, key: u64, value: u64) { + pub fn set_property(&mut self, key: u64, value: Property) { if let Some(ref mut properties) = self.properties { properties.insert(key, value); } else { @@ -429,8 +429,8 @@ impl Instruction { } #[must_use] - pub fn get_property(&self, key: &u64) -> Option { - self.properties.as_ref()?.get(key).copied() + pub fn get_property(&self, key: &u64) -> Option<&Property> { + self.properties.as_ref()?.get(key) } #[must_use] @@ -441,7 +441,7 @@ impl Instruction { } #[must_use] - pub fn get_property_or(&self, key: &u64, default: u64) -> u64 { + pub fn get_property_or<'a>(&'a self, key: &u64, default: &'a Property) -> &'a Property { self.get_property(key).unwrap_or(default) } } @@ -465,6 +465,7 @@ pub struct InstructionConstraint { arity: Option, error_rate_fn: Option>, properties: FxHashSet, + property_bounds: FxHashMap>, } impl InstructionConstraint { @@ -481,6 +482,7 @@ impl InstructionConstraint { arity, error_rate_fn, properties: FxHashSet::default(), + property_bounds: FxHashMap::default(), } } @@ -489,6 +491,11 @@ impl InstructionConstraint { self.properties.insert(property); } + /// Adds a numeric property bound requirement to the constraint. + pub fn add_property_bound(&mut self, property: u64, bound: ConstraintBound) { + self.property_bounds.insert(property, bound); + } + /// Checks if the constraint requires a specific property. #[must_use] pub fn has_property(&self, property: &u64) -> bool { @@ -501,6 +508,12 @@ impl InstructionConstraint { &self.properties } + /// Returns the map of numeric property bounds. + #[must_use] + pub fn property_bounds(&self) -> &FxHashMap> { + &self.property_bounds + } + /// Returns the instruction ID this constraint applies to. #[must_use] pub fn id(&self) -> u64 { @@ -570,6 +583,25 @@ impl InstructionConstraint { } } + for (prop, bound) in &self.property_bounds { + let Some(value) = instruction.get_property(prop) else { + return false; + }; + + let value = match value { + Property::Int(v) => match v.to_f64() { + Some(v) => v, + None => return false, + }, + Property::Float(v) => *v, + _ => return false, + }; + + if !bound.evaluate(&value) { + return false; + } + } + true } } diff --git a/source/qre/src/isa/property_keys.rs b/source/qre/src/isa/property_keys.rs index 96dcf664bd..952d1bfcff 100644 --- a/source/qre/src/isa/property_keys.rs +++ b/source/qre/src/isa/property_keys.rs @@ -50,6 +50,8 @@ define_properties! { SURFACE_CODE_ONE_QUBIT_TIME_FACTOR, SURFACE_CODE_TWO_QUBIT_TIME_FACTOR, ACCELERATION, + ATOM_SPACING, + VELOCITY, NUM_TS_PER_ROTATION, RUNTIME_SINGLE_SHOT, EXPECTED_SHOTS, diff --git a/source/qre/src/isa/provenance.rs b/source/qre/src/isa/provenance.rs index 5f68ca180e..2c9b275001 100644 --- a/source/qre/src/isa/provenance.rs +++ b/source/qre/src/isa/provenance.rs @@ -5,7 +5,28 @@ use std::sync::{Arc, RwLock}; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{Encoding, ISA, ISARequirements, Instruction, ParetoFrontier3D}; +use crate::{ + Encoding, ISA, ISARequirements, Instruction, ParetoFrontier3D, Property, float_to_bits, +}; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum PropertyGroupKey { + Bool(bool), + Int(i64), + Float(u64), + Str(String), +} + +impl From<&Property> for PropertyGroupKey { + fn from(value: &Property) -> Self { + match value { + Property::Bool(v) => Self::Bool(*v), + Property::Int(v) => Self::Int(*v), + Property::Float(v) => Self::Float(float_to_bits(*v)), + Property::Str(v) => Self::Str(v.clone()), + } + } +} pub struct ProvenanceGraph { nodes: Vec, @@ -128,15 +149,20 @@ impl ProvenanceGraph { // Sub-partition by encoding and property keys to avoid comparing // incompatible instructions (Risk R2 mitigation) #[allow(clippy::type_complexity)] - let mut sub_groups: FxHashMap<(Encoding, Vec<(u64, u64)>), Vec> = - FxHashMap::default(); + let mut sub_groups: FxHashMap< + (Encoding, Vec<(u64, PropertyGroupKey)>), + Vec, + > = FxHashMap::default(); for &idx in &node_indices { let instr = &self.nodes[idx].instruction; - let mut prop_vec: Vec<(u64, u64)> = instr + let mut prop_vec: Vec<(u64, PropertyGroupKey)> = instr .properties .as_ref() .map(|p| { - let mut v: Vec<_> = p.iter().map(|(&k, &v)| (k, v)).collect(); + let mut v: Vec<_> = p + .iter() + .map(|(&k, v)| (k, PropertyGroupKey::from(v))) + .collect(); v.sort_unstable(); v })