diff --git a/packages/circuit/quri_parts/circuit/gate_names.py b/packages/circuit/quri_parts/circuit/gate_names.py index c2367e12..dc4f593b 100644 --- a/packages/circuit/quri_parts/circuit/gate_names.py +++ b/packages/circuit/quri_parts/circuit/gate_names.py @@ -53,7 +53,6 @@ U1: Literal["U1"] = "U1" U2: Literal["U2"] = "U2" U3: Literal["U3"] = "U3" -Measurement: Literal["Measurement"] = "Measurement" SINGLE_QUBIT_GATE_NAMES: set[SingleQubitGateNameType] = { Identity, @@ -130,10 +129,22 @@ def is_multi_qubit_gate_name(gate_name: str) -> TypeGuard[MultiQubitGateNameType #: A set of strings representing gate names. NonParametricGateNameType: TypeAlias = Union[ - SingleQubitGateNameType, TwoQubitGateNameType, MultiQubitGateNameType + SingleQubitGateNameType, + TwoQubitGateNameType, + ThreeQubitGateNameType, + MultiQubitGateNameType, ] +def is_non_parametric_gate_name(gate_name: str) -> TypeGuard[NonParametricGateNameType]: + return gate_name in ( + SINGLE_QUBIT_GATE_NAMES + | TWO_QUBIT_GATE_NAMES + | THREE_QUBIT_GATE_NAMES + | MULTI_QUBIT_GATE_NAMES + ) + + UnitaryMatrixGateNameType: TypeAlias = Literal["UnitaryMatrix"] UnitaryMatrix: Literal["UnitaryMatrix"] = "UnitaryMatrix" @@ -171,6 +182,11 @@ def is_parametric_gate_name(gate_name: str) -> TypeGuard[ParametricGateNameType] return gate_name in PARAMETRIC_GATE_NAMES +MeasurementGateNameType: TypeAlias = Literal["Measurement"] +Measurement: Literal["Measurement"] = "Measurement" +MEASUREMENT_GATE_NAMES: set[MeasurementGateNameType] = {Measurement} + + #: Valid Pauli gate names. PauliNameType: TypeAlias = Literal["X", "Y", "Z", "Pauli"] @@ -230,6 +246,7 @@ def is_pauli_name(gate_name: str) -> TypeGuard[PauliNameType]: MultiQubitGateNameType, UnitaryMatrixGateNameType, ParametricGateNameType, + MeasurementGateNameType, ] GATE_NAMES: set[GateNameType] = ( @@ -239,6 +256,7 @@ def is_pauli_name(gate_name: str) -> TypeGuard[PauliNameType]: | MULTI_QUBIT_GATE_NAMES | UNITARY_MATRIX_GATE_NAMES | PARAMETRIC_GATE_NAMES + | MEASUREMENT_GATE_NAMES ) diff --git a/packages/circuit/quri_parts/circuit/topology/square_lattice.py b/packages/circuit/quri_parts/circuit/topology/square_lattice.py index 2fac89c1..1f41eafc 100644 --- a/packages/circuit/quri_parts/circuit/topology/square_lattice.py +++ b/packages/circuit/quri_parts/circuit/topology/square_lattice.py @@ -8,7 +8,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Mapping, Sequence +from collections.abc import Collection, Mapping, Sequence from typing import Callable, Optional from typing_extensions import TypeAlias @@ -46,6 +46,19 @@ def __init__( f"The same coordinates were specified multiple times: {v}." ) self._coord_to_qubit[v] = k + self._xsize, self._ysize = xsize, ysize + + @property + def xsize(self) -> int: + return self._xsize + + @property + def ysize(self) -> int: + return self._ysize + + @property + def qubits(self) -> Collection[int]: + return self._qubit_to_coord.keys() def get_qubit(self, coord: Coordinate) -> int: """Returns qubit index corresponding to the given coordinate.""" diff --git a/packages/circuit/quri_parts/circuit/transpile/__init__.py b/packages/circuit/quri_parts/circuit/transpile/__init__.py index 23816bf4..86a6969e 100644 --- a/packages/circuit/quri_parts/circuit/transpile/__init__.py +++ b/packages/circuit/quri_parts/circuit/transpile/__init__.py @@ -62,6 +62,8 @@ from .gateset import ( CliffordConversionTranspiler, GateSetConversionTranspiler, + ParametricRX2RZHTranspiler, + ParametricRY2RZHTranspiler, RotationConversionTranspiler, RX2RYRZTranspiler, RX2RZHTranspiler, @@ -87,6 +89,7 @@ ParallelDecomposer, ParametricCircuitTranspiler, ParametricCircuitTranspilerProtocol, + ParametricSequentialTranspiler, ParametricTranspiler, SequentialTranspiler, ) @@ -195,9 +198,20 @@ def __init__(self, epsilon: float = 1.0e-9): #: CircuitTranspiler to transpile a QuntumCircuit into another #: QuantumCircuit containing only basic gates for STAR architecture (H, RZ, and CNOT). #: (UnitaryMatrix gate for 3 or more qubits are not decomposed.) -STARSetTranspiler: Callable[ - [], CircuitTranspiler -] = lambda: GateSetConversionTranspiler([gate_names.H, gate_names.RZ, gate_names.CNOT]) +STARSetTranspiler: Callable[[], CircuitTranspiler] = lambda: SequentialTranspiler( + [ + RX2RZHTranspiler(), + RY2RZHTranspiler(), + GateSetConversionTranspiler( + [ + gate_names.H, + gate_names.S, + gate_names.RZ, + gate_names.CNOT, + ] + ), + ] +) __all__ = [ @@ -223,6 +237,13 @@ def __init__(self, epsilon: float = 1.0e-9): "ParallelDecomposer", "PauliDecomposeTranspiler", "PauliRotationDecomposeTranspiler", + "ParametricPauliRotationDecomposeTranspiler", + "ParametricTranspiler", + "ParametricCircuitTranspiler", + "ParametricCircuitTranspilerProtocol", + "ParametricRX2RZHTranspiler", + "ParametricRY2RZHTranspiler", + "ParametricSequentialTranspiler", "QubitRemappingTranspiler", "RotationConversionTranspiler", "RX2RYRZTranspiler", diff --git a/packages/circuit/quri_parts/circuit/transpile/gateset.py b/packages/circuit/quri_parts/circuit/transpile/gateset.py index 51fe30e6..f94c8061 100644 --- a/packages/circuit/quri_parts/circuit/transpile/gateset.py +++ b/packages/circuit/quri_parts/circuit/transpile/gateset.py @@ -12,7 +12,13 @@ from math import pi from typing import cast -from quri_parts.circuit import ImmutableQuantumCircuit, QuantumCircuit +from quri_parts.circuit import ( + ImmutableQuantumCircuit, + LinearMappedParametricQuantumCircuit, + LinearParameterMapping, + ParametricQuantumCircuitProtocol, + QuantumCircuit, +) from quri_parts.circuit import gates as gf from quri_parts.circuit.gate import QuantumGate from quri_parts.circuit.gate_names import ( @@ -32,6 +38,10 @@ GateNameType, H, Identity, + ParametricPauliRotation, + ParametricRX, + ParametricRY, + ParametricRZ, Pauli, PauliRotation, S, @@ -86,6 +96,7 @@ CircuitTranspiler, CircuitTranspilerProtocol, GateKindDecomposer, + ParametricCircuitTranspilerProtocol, SequentialTranspiler, ) from .unitary_matrix_decomposer import ( @@ -301,6 +312,85 @@ def decompose(self, gate: QuantumGate) -> Sequence[QuantumGate]: ] +class ParametricRX2RZHTranspiler(ParametricCircuitTranspilerProtocol): + def __call__( + self, circuit: ParametricQuantumCircuitProtocol + ) -> LinearMappedParametricQuantumCircuit: + ret = LinearMappedParametricQuantumCircuit( + circuit.qubit_count, circuit.cbit_count + ) + ret._param_mapping = LinearParameterMapping(circuit.param_mapping.in_params) + pmap = circuit.param_mapping.mapping + + for gate, param in circuit.primitive_circuit().gates_and_params: + if isinstance(gate, QuantumGate): + ret.add_gate(gate) + else: + if param is None: + raise ValueError("Parametric gate with no Parameter: {gate}") + + qubit = gate.target_indices[0] + if gate.name == ParametricRX: + ret.add_H_gate(qubit) + ret.add_ParametricRZ_gate(qubit, pmap[param]) + ret.add_H_gate(qubit) + elif gate.name == ParametricRY: + ret.add_ParametricRY_gate(qubit, pmap[param]) + elif gate.name == ParametricRZ: + ret.add_ParametricRZ_gate(qubit, pmap[param]) + elif gate.name == ParametricPauliRotation: + ret.add_ParametricPauliRotation_gate( + gate.target_indices, + gate.pauli_ids, + pmap[param], + ) + else: + raise ValueError(f"Unsupported parametric gate: {gate.name}") + + return ret + + +class ParametricRY2RZHTranspiler(ParametricCircuitTranspilerProtocol): + def __call__( + self, circuit: ParametricQuantumCircuitProtocol + ) -> LinearMappedParametricQuantumCircuit: + ret = LinearMappedParametricQuantumCircuit( + circuit.qubit_count, circuit.cbit_count + ) + ret._param_mapping = LinearParameterMapping(circuit.param_mapping.in_params) + pmap = circuit.param_mapping.mapping + + for gate, param in circuit.primitive_circuit().gates_and_params: + if isinstance(gate, QuantumGate): + ret.add_gate(gate) + else: + if param is None: + raise ValueError("Parametric gate with no Parameter: {gate}") + + qubit = gate.target_indices[0] + if gate.name == ParametricRX: + ret.add_ParametricRX_gate(qubit, pmap[param]) + elif gate.name == ParametricRY: + qubit = gate.target_indices[0] + ret.add_RZ_gate(qubit, -pi / 2.0) + ret.add_H_gate(qubit) + ret.add_ParametricRZ_gate(qubit, pmap[param]) + ret.add_H_gate(qubit) + ret.add_RZ_gate(qubit, pi / 2.0) + elif gate.name == ParametricRZ: + ret.add_ParametricRZ_gate(qubit, pmap[param]) + elif gate.name == ParametricPauliRotation: + ret.add_ParametricPauliRotation_gate( + gate.target_indices, + gate.pauli_ids, + pmap[param], + ) + else: + raise ValueError(f"Unsupported parametric gate: {gate.name}") + + return ret + + class IdentityTranspiler(CircuitTranspilerProtocol): """A CircuitTranspiler returns the same circuit as the input.""" diff --git a/packages/circuit/quri_parts/circuit/transpile/transpiler.py b/packages/circuit/quri_parts/circuit/transpile/transpiler.py index a98ff615..bb2052a1 100644 --- a/packages/circuit/quri_parts/circuit/transpile/transpiler.py +++ b/packages/circuit/quri_parts/circuit/transpile/transpiler.py @@ -234,3 +234,15 @@ def __call__( ret.extend(self._transpiler(cc).gates) return ret + + +class ParametricSequentialTranspiler(ParametricCircuitTranspilerProtocol): + def __init__(self, transpilers: Sequence[ParametricCircuitTranspiler]): + self._transpilers = transpilers + + def __call__( + self, circuit: ParametricQuantumCircuitProtocol + ) -> ParametricQuantumCircuitProtocol: + for transpiler in self._transpilers: + circuit = transpiler(circuit) + return circuit diff --git a/packages/circuit/tests/circuit/transpile/test_gate_kind_decomposer.py b/packages/circuit/tests/circuit/transpile/test_gate_kind_decomposer.py index 5178aa46..bd90341a 100644 --- a/packages/circuit/tests/circuit/transpile/test_gate_kind_decomposer.py +++ b/packages/circuit/tests/circuit/transpile/test_gate_kind_decomposer.py @@ -701,6 +701,11 @@ def test_starset_transpile(self) -> None: transpiled = STARSetTranspiler()(circuit) target_set = {gate.name for gate in transpiled.gates} - expect_set = {gate_names.H, gate_names.RZ, gate_names.CNOT} + expect_set = { + gate_names.H, + gate_names.S, + gate_names.RZ, + gate_names.CNOT, + } assert target_set <= expect_set diff --git a/packages/circuit/tests/circuit/transpile/test_gateset.py b/packages/circuit/tests/circuit/transpile/test_gateset.py index 4df4a4ef..13fe6090 100644 --- a/packages/circuit/tests/circuit/transpile/test_gateset.py +++ b/packages/circuit/tests/circuit/transpile/test_gateset.py @@ -8,10 +8,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Union + import numpy as np from quri_parts.circuit import ( ImmutableQuantumCircuit, + NonParametricQuantumCircuit, + ParametricQuantumCircuit, + ParametricQuantumCircuitProtocol, + ParametricQuantumGate, QuantumCircuit, QuantumGate, gate_names, @@ -21,6 +27,8 @@ from quri_parts.circuit.transpile import ( CliffordConversionTranspiler, GateSetConversionTranspiler, + ParametricRX2RZHTranspiler, + ParametricRY2RZHTranspiler, RotationConversionTranspiler, RX2RYRZTranspiler, RX2RZHTranspiler, @@ -30,18 +38,29 @@ ) -def _gates_close(x: QuantumGate, y: QuantumGate) -> bool: - return ( - x.name == y.name - and x.target_indices == y.target_indices - and x.control_indices == y.control_indices - and np.allclose(x.params, y.params) - and x.pauli_ids == y.pauli_ids - and np.allclose(x.unitary_matrix, y.unitary_matrix) - ) +def _gates_close( + x: Union[QuantumGate, ParametricQuantumGate], + y: Union[QuantumGate, ParametricQuantumGate], +) -> bool: + if isinstance(x, ParametricQuantumGate) and isinstance(y, ParametricQuantumGate): + return x == y + elif isinstance(x, QuantumGate) and isinstance(y, QuantumGate): + return ( + x.name == y.name + and x.target_indices == y.target_indices + and x.control_indices == y.control_indices + and np.allclose(x.params, y.params) + and x.pauli_ids == y.pauli_ids + and np.allclose(x.unitary_matrix, y.unitary_matrix) + ) + else: + return False -def _circuit_close(x: ImmutableQuantumCircuit, y: ImmutableQuantumCircuit) -> bool: +def _circuit_close( + x: Union[NonParametricQuantumCircuit, ParametricQuantumCircuitProtocol], + y: Union[NonParametricQuantumCircuit, ParametricQuantumCircuitProtocol], +) -> bool: return len(x.gates) == len(y.gates) and all( _gates_close(a, b) for a, b in zip(x.gates, y.gates) ) @@ -294,6 +313,34 @@ def test_rz2rxry_transpile(self) -> None: assert _circuit_close(transpiled, expect) +class TestParametricRotationKindDecompose: + def test_parametricrx2rzh_transpile(self) -> None: + circuit = ParametricQuantumCircuit(1) + circuit.add_ParametricRX_gate(0) + transpiled = ParametricRX2RZHTranspiler()(circuit) + + expect = ParametricQuantumCircuit(1) + expect.add_H_gate(0) + expect.add_ParametricRZ_gate(0) + expect.add_H_gate(0) + + assert _circuit_close(transpiled, expect) + + def test_parametricry2rzh_transpile(self) -> None: + circuit = ParametricQuantumCircuit(1) + circuit.add_ParametricRY_gate(0) + transpiled = ParametricRY2RZHTranspiler()(circuit) + + expect = ParametricQuantumCircuit(1) + expect.add_RZ_gate(0, -np.pi / 2.0) + expect.add_H_gate(0) + expect.add_ParametricRZ_gate(0) + expect.add_H_gate(0) + expect.add_RZ_gate(0, np.pi / 2.0) + + assert _circuit_close(transpiled, expect) + + class TestRotationConversion: def test_rxryrz_transpile(self) -> None: circuit = _rotation_circuit(np.pi / 7.0) diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 3894a4ba..03998237 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -30,6 +30,7 @@ typing-extensions = "^4.1.1" numpy = ">=1.22.0" quri-parts-circuit = "*" scipy = "^1.11.3" +networkx = "*" [tool.poetry.group.dev.dependencies] quri-parts-circuit = {path = "../circuit", develop = true} diff --git a/packages/core/quri_parts/backend/cost_estimator.py b/packages/core/quri_parts/backend/cost_estimator.py new file mode 100644 index 00000000..f9b39ce8 --- /dev/null +++ b/packages/core/quri_parts/backend/cost_estimator.py @@ -0,0 +1,131 @@ +from collections.abc import Collection, Mapping +from typing import Callable, cast + +from quri_parts.circuit import NonParametricQuantumCircuit, QuantumGate +from quri_parts.circuit.gate_names import GateNameType + +from .device import DeviceProperty +from .units import TimeUnit, TimeValue + + +def _gate_weighted_depth( + circuit: NonParametricQuantumCircuit, + gate_weight: Callable[[QuantumGate], float], +) -> float: + qubit_depth: dict[int, float] = {} + + for gate in circuit.gates: + qubits = tuple(gate.control_indices) + tuple(gate.target_indices) + depth = gate_weight(gate) + max(qubit_depth.get(q, 0.0) for q in qubits) + for q in qubits: + qubit_depth[q] = depth + + return max(qubit_depth.values()) if qubit_depth else 0.0 + + +def _gate_kind_weighted_depth( + circuit: NonParametricQuantumCircuit, + gate_kind_weight: Mapping[str, float], + default_weight: float = 0.0, +) -> float: + return _gate_weighted_depth( + circuit, lambda gate: gate_kind_weight.get(gate.name, default_weight) + ) + + +def _estimate_gate_latency( + circuit: NonParametricQuantumCircuit, + device: DeviceProperty, + kinds: Collection[GateNameType] = [], +) -> TimeValue: + latency = 0.0 + for gate in circuit.gates: + lat = device.gate_property(gate).gate_time + if lat is None: + raise ValueError("Contains gate with unknown gate gate_time: {gate}") + if kinds and gate.name not in kinds: + continue + latency += lat.in_ns() + return TimeValue(value=latency, unit=TimeUnit.NANOSECOND) + + +def estimate_circuit_latency( + circuit: NonParametricQuantumCircuit, + device: DeviceProperty, +) -> TimeValue: + """Estimates the execution time for processing the given quantum circuit on + the given device. + + Args: + circuit: NonParametricQuantumCircuit to be estimated. + device: DeviceProperty of the device to execute the circuit. + + Returns: + Estimated latency of the circuit in nano seconds when executing on the given + device. + """ + for gate in circuit.gates: + if device.gate_property(gate).gate_time is None: + raise ValueError(f"Contains gate with unknown gate_time: {gate}") + + return TimeValue( + value=_gate_weighted_depth( + circuit, + lambda gate: cast(TimeValue, device.gate_property(gate).gate_time).in_ns(), + ), + unit=TimeUnit.NANOSECOND, + ) + + +def _estimate_gate_fidelity( + circuit: NonParametricQuantumCircuit, + device: DeviceProperty, + kinds: Collection[GateNameType] = [], +) -> float: + fidelity = 1.0 + for gate in circuit.gates: + error = device.gate_property(gate).gate_error + if error is None: + raise ValueError(f"Contains gate with unknown gate_error: {gate}") + if kinds and gate.name not in kinds: + continue + fidelity *= 1.0 - error + return fidelity + + +def _estimate_background_fidelity( + circuit: NonParametricQuantumCircuit, device: DeviceProperty +) -> float: + if device.background_error is None: + return 1.0 + circuit_latency = estimate_circuit_latency(circuit, device).value + error, latency = device.background_error + + return cast( + float, + (1.0 - error) ** ((circuit_latency * circuit.qubit_count) / latency.in_ns()), + ) + + +def estimate_circuit_fidelity( + circuit: NonParametricQuantumCircuit, + device: DeviceProperty, + background_error: bool = True, +) -> float: + """Estimates the fidelity of processing the given quantum circuit on the + given device. + + Args: + circuit: NonParametricQuantumCircuit to be estimated. + device: DeviceProperty of the device to execute the circuit. + background_error: Specifies whether the background error is factored into the + fidelity of the circuit. True by default. + + Returns: + Estimated fidelity of the circuit when executing on the given device. + """ + background_fidelity = ( + _estimate_background_fidelity(circuit, device) if background_error else 1.0 + ) + gate_fidelity = _estimate_gate_fidelity(circuit, device) + return gate_fidelity * background_fidelity diff --git a/packages/core/quri_parts/backend/device.py b/packages/core/quri_parts/backend/device.py new file mode 100644 index 00000000..60d7082e --- /dev/null +++ b/packages/core/quri_parts/backend/device.py @@ -0,0 +1,150 @@ +from collections.abc import Collection, Mapping, Sequence +from dataclasses import dataclass +from typing import Optional + +import networkx as nx + +from quri_parts.circuit import QuantumGate +from quri_parts.circuit.gate_names import GateNameType, is_non_parametric_gate_name +from quri_parts.circuit.noise import NoiseModel +from quri_parts.circuit.transpile import CircuitTranspiler, ParametricCircuitTranspiler + +from .units import FrequencyValue, TimeValue + + +@dataclass(frozen=True) +class QubitProperty: + """Noise property of a qubit. + + Args: + T1 (TimeValue, optional): t1 relaxation time + T2 (TimeValue, optional): t2 relaxation time + frequency (FrequencyValue,optional): qubit frequency + prob_meas0_on_state1 (float, optional): readout error probability of + measuring 0 when the state is 1 + prob_meas1_on_state0 (float, optional): readout error probability of + measuring 1 when the state is 0 + readout_time (TimeValue, optional): time duration on measurement. + name (str, optional): name of the qubit + """ + + T1: Optional[TimeValue] = None + T2: Optional[TimeValue] = None + frequency: Optional[FrequencyValue] = None + prob_meas0_on_state1: Optional[float] = None + prob_meas1_on_state0: Optional[float] = None + readout_time: Optional[TimeValue] = None + name: Optional[str] = None + + +@dataclass(frozen=True) +class GateProperty: + """Noise property of a gate. + + Args: + gate (GateNameType): gate name + qubits (Sequence[int]): target qubits for the gate. The order is control_index0, + control_index1, ..., target_index0, ... + gate_error (float, optional): 1 - fidelity of the gate operation + gate_time (float, optional): time duration of the gate operation + name (str, optional): name of the gate + """ + + gate: GateNameType + qubits: Sequence[int] + gate_error: Optional[float] = None + gate_time: Optional[TimeValue] = None + name: Optional[str] = None + + +@dataclass +class DeviceProperty: + """Stores properties of a quantum device for circuit cost estimation. + + Args: + qubit_count (int): Number of qubits. + qubits (Sequence[int]): Qubit indices. + qubit_graph (newtorkx.Graph): Topology of qubit connections. + qubit_properties (Mapping[int, QubitProperty]): Mapping from qubit index to + QubitProperty. + native_gates (Collection[GateNameType]): Names of supported gates. + gate_properties (Collection[GateProperty]): Collection of GateProperty. + physical_qubit_count: (int, optional): Number of physical qubits. + background_error: (tuple[float, TimeValue], optional): The errors that + occur with respect to the passage of time for each qubit, regardless + of the application of gates, etc. It must be given together with the + time unit. + name (str, optional): Name of the device. + provider (str, optional): Provider of the device. + transpiler (CircuitTranspiler, optional): CircuitTranspiler for converting + to an instruction sequence supported by the target device. + parametric_transpiler (ParametricCircuitTranspiler, optional): + ParametricCircuitTranspiler for converting to an instruction sequence + supported by the target device. + noise_model (NoiseModel, optional): Noise models that reproduce device + behaviour. + """ + + qubit_count: int + qubits: Sequence[int] + qubit_graph: nx.Graph + qubit_properties: Mapping[int, QubitProperty] + native_gates: Sequence[GateNameType] + _gate_properties: Mapping[tuple[GateNameType, tuple[int, ...]], GateProperty] + physical_qubit_count: Optional[int] = None + background_error: Optional[tuple[float, TimeValue]] = None + name: Optional[str] = None + provider: Optional[str] = None + + transpiler: Optional[CircuitTranspiler] = None + parametric_transpiler: Optional[ParametricCircuitTranspiler] = None + noise_model: Optional[NoiseModel] = None + + def __init__( + self, + qubit_count: int, + qubits: Sequence[int], + qubit_graph: nx.Graph, + qubit_properties: Mapping[int, QubitProperty], + native_gates: Collection[GateNameType], + gate_properties: Collection[GateProperty], + physical_qubit_count: Optional[int] = None, + background_error: Optional[tuple[float, TimeValue]] = None, + name: Optional[str] = None, + provider: Optional[str] = None, + transpiler: Optional[CircuitTranspiler] = None, + parametric_transpiler: Optional[ParametricCircuitTranspiler] = None, + noise_model: Optional[NoiseModel] = None, + ) -> None: + self.qubit_count = qubit_count + self.qubits = tuple(qubits) + self.qubit_graph = qubit_graph + self.qubit_properties = qubit_properties + self.native_gates = tuple(native_gates) + self._gate_properties = { + (prop.gate, tuple(prop.qubits)): prop for prop in gate_properties + } + self.physical_qubit_count = physical_qubit_count + self.background_error = background_error + self.name = name + self.provider = provider + self.transpiler = transpiler + self.parametric_transpiler = parametric_transpiler + self.noise_model = noise_model + + def gate_property(self, quantum_gate: QuantumGate) -> GateProperty: + """Returns GateProperty of the device corresponding to the given + QuantumGate. + + If the given quantum gate is not specified with qubits, the + GateProperty for the kind of the quantum gate is searched. + """ + + if not is_non_parametric_gate_name(quantum_gate.name): + raise ValueError(f"Unsupported gate kind: {quantum_gate.name}") + gate = ( + quantum_gate.name, + tuple(quantum_gate.control_indices) + tuple(quantum_gate.target_indices), + ) + gate = gate if gate in self._gate_properties else (gate[0], ()) + return self._gate_properties[gate] diff --git a/packages/core/quri_parts/backend/devices/clifford_t_device.py b/packages/core/quri_parts/backend/devices/clifford_t_device.py new file mode 100644 index 00000000..f7af6fc6 --- /dev/null +++ b/packages/core/quri_parts/backend/devices/clifford_t_device.py @@ -0,0 +1,149 @@ +from math import ceil, log2, sqrt +from typing import cast + +import networkx as nx + +from quri_parts.backend.device import DeviceProperty, GateProperty, QubitProperty +from quri_parts.backend.units import TimeValue +from quri_parts.circuit import gate_names + + +def generate_device_property( + qubit_count: int, + code_distance: int, + qec_cycle: TimeValue, + delta_sk: float, + mode_block: str, + physical_error_rate: float = 0.0, +) -> DeviceProperty: + """Generate DeviceInfo object for Clifford + T architecture devices. + + Args: + qubit_count: Number of logical qubits. + physical_error_rate: Error rate of physical qubit operations. + code_distance: Code distance of the quantum error correction code. + qec_cycle: Time duration of each syndrome measurement for quantum + error correction (without code distance dependency). + delta_sk: Required accuracy of sk decompositon of each rotation gate. + mode_block: Layout plan for each area on the lattice. One of "fast", + "intermediate", or "compact" + + Returns: + DeviceInfo object representing the target Clifford + T architecture device. + + References: + https://arxiv.org/abs/2303.13181 + """ + + # cf. https://arxiv.org/abs/2303.13181 + if mode_block == "fast": + total_logical_patches = ceil(2 * qubit_count + sqrt(8 * qubit_count) + 1) + latency_1T = 1 + patches_for_magic_state_factory = 121 + elif mode_block == "intermediate": + total_logical_patches = 2 * qubit_count + 4 + latency_1T = 5 + patches_for_magic_state_factory = 33 + elif mode_block == "compact": + total_logical_patches = ceil(1.5 * qubit_count + 3) + latency_1T = 9 + patches_for_magic_state_factory = 22 + else: + raise ValueError("Invalid mode for encoding blocks.") + + # https://arxiv.org/abs/2303.13181 (p11-14) + def logical_error_model(ci: float, pthi: float, p: float, d: int) -> float: + return cast(float, ci * (p / pthi) ** ((d + 1.0) / 2.0)) + + def logical_error_round(p: float, d: int) -> float: + plz = logical_error_model(ci=0.067976, pthi=0.0038510, p=p, d=d) + plx = logical_error_model(ci=0.081997, pthi=0.0041612, p=p, d=d) + return plz + plx + + logical_fidelity_round = 1.0 - logical_error_round( + p=physical_error_rate, d=code_distance + ) + + num_Ts_for_1q = ceil(3 * log2(1 / delta_sk)) + latency_1q = latency_1T * num_Ts_for_1q + + qubits = list(range(qubit_count)) + qubit_properties = {q: QubitProperty() for q in qubits} + gate_properties = [ + GateProperty( + gate_names.H, + [], + gate_error=0.0, + gate_time=TimeValue( + value=3.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.S, + [], + gate_error=0.0, + # TODO Confirm latency value + gate_time=TimeValue( + value=2.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.T, + [], + gate_error=0.0, + # TODO Confirm latency value + gate_time=TimeValue( + value=latency_1T * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.CNOT, + [], + gate_error=0.0, + gate_time=TimeValue( + value=2.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.RZ, + [], + gate_error=delta_sk, + gate_time=TimeValue( + value=latency_1q * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.ParametricRZ, + [], + gate_error=delta_sk, + gate_time=TimeValue( + value=latency_1q * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + ] + + total_patches = ( + total_logical_patches + patches_for_magic_state_factory + ) # (data+ancilla)+factory + + data_total_qubit_ratio = total_patches / qubit_count + qec_fidelity_per_qec_cycle = logical_fidelity_round**data_total_qubit_ratio + + # 2d^2 physical qubits per single patch, including syndrome measurement qubits + physical_qubit_count = (2 * code_distance**2) * total_patches + + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=nx.complete_graph(qubit_count), + qubit_properties=qubit_properties, + native_gates=[ + gate_names.H, + gate_names.S, + gate_names.T, + gate_names.CNOT, + ], + gate_properties=gate_properties, + physical_qubit_count=physical_qubit_count, + background_error=(1.0 - qec_fidelity_per_qec_cycle, qec_cycle), + ) diff --git a/packages/core/quri_parts/backend/devices/nisq_iontrap_device.py b/packages/core/quri_parts/backend/devices/nisq_iontrap_device.py new file mode 100644 index 00000000..ba980c26 --- /dev/null +++ b/packages/core/quri_parts/backend/devices/nisq_iontrap_device.py @@ -0,0 +1,86 @@ +from collections.abc import Collection +from typing import Optional + +import networkx as nx + +from quri_parts.backend.device import DeviceProperty, GateProperty, QubitProperty +from quri_parts.backend.units import TimeValue +from quri_parts.circuit import gate_names +from quri_parts.circuit.gate_names import GateNameType +from quri_parts.circuit.transpile import GateSetConversionTranspiler + + +def generate_device_property( + qubit_count: int, + native_gates: Collection[GateNameType], + gate_error_1q: float, + gate_error_2q: float, + gate_error_meas: float, + gate_time_1q: TimeValue, + gate_time_2q: TimeValue, + gate_time_meas: TimeValue, + t1: Optional[TimeValue] = None, + t2: Optional[TimeValue] = None, +) -> DeviceProperty: + """Generate DeviceProperty object for a typical NISQ trapped ion device. + + Assumes that the device's qubits are all-to-all connected and that a subset of + the gates natively supported by QURI Parts can be used as the native gates. + + Args: + qubit_count: Number of qubits. + native_gates: Native gates supported by the device. + gate_error_1q: Error rate of single qubit gate operations. + gate_error_2q: Error rate of two qubit gate operations. + gate_error_meas: Error rate of readout operations. + gate_time_1q: Latency of single qubit gate operations. + gate_time_2q: Latency of two qubit gate operations. + gate_time_meas: Latency of readout operations. + t1: T1 coherence time. + t2: T2 coherence time. + """ + + native_gate_set = set(native_gates) + gates_1q = native_gate_set & gate_names.SINGLE_QUBIT_GATE_NAMES + gates_2q = native_gate_set & gate_names.TWO_QUBIT_GATE_NAMES + + if gates_1q | gates_2q != native_gate_set: + raise ValueError( + "Only single and two qubit gates are supported as native gates" + ) + + qubits = list(range(qubit_count)) + qubit_properties = {q: QubitProperty() for q in qubits} + gate_properties = [ + GateProperty( + gate_names.Measurement, + (), + gate_error=gate_error_meas, + gate_time=gate_time_meas, + ) + ] + gate_properties.extend( + [ + GateProperty(name, (), gate_error=gate_error_1q, gate_time=gate_time_1q) + for name in gates_1q + ] + ) + gate_properties.extend( + [ + GateProperty(name, (), gate_error=gate_error_2q, gate_time=gate_time_2q) + for name in gates_2q + ] + ) + + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=nx.complete_graph(qubit_count), + qubit_properties=qubit_properties, + native_gates=native_gates, + gate_properties=gate_properties, + physical_qubit_count=qubit_count, + # TODO Calculate backgraound error from t1 and t2 + background_error=None, + transpiler=GateSetConversionTranspiler(native_gates), + ) diff --git a/packages/core/quri_parts/backend/devices/nisq_spcond_lattice.py b/packages/core/quri_parts/backend/devices/nisq_spcond_lattice.py new file mode 100644 index 00000000..0bc66025 --- /dev/null +++ b/packages/core/quri_parts/backend/devices/nisq_spcond_lattice.py @@ -0,0 +1,111 @@ +from collections.abc import Collection +from typing import Optional + +import networkx as nx + +from quri_parts.backend.device import DeviceProperty, GateProperty, QubitProperty +from quri_parts.backend.units import TimeValue +from quri_parts.circuit import gate_names +from quri_parts.circuit.gate_names import GateNameType +from quri_parts.circuit.topology import ( + SquareLattice, + SquareLatticeSWAPInsertionTranspiler, +) +from quri_parts.circuit.transpile import ( + GateSetConversionTranspiler, + SequentialTranspiler, +) + + +def generate_device_property( + lattice: SquareLattice, + native_gates: Collection[GateNameType], + gate_error_1q: float, + gate_error_2q: float, + gate_error_meas: float, + gate_time_1q: TimeValue, + gate_time_2q: TimeValue, + gate_time_meas: TimeValue, + t1: Optional[TimeValue] = None, + t2: Optional[TimeValue] = None, +) -> DeviceProperty: + """Generate DeviceProperty object for a typical NISQ superconducting qubit + device. + + Assumes that the device's qubits are connected as square lattice with no defects + and that a subset of the gates natively supported by QURI Parts can be used as + the native gates. + + Args: + lattice: SquareLattice instance representing the device qubits connectivity. + native_gates: Native gates supported by the device. + gate_error_1q: Error rate of single qubit gate operations. + gate_error_2q: Error rate of two qubit gate operations. + gate_error_meas: Error rate of readout operations. + gate_time_1q: Latency of single qubit gate operations. + gate_time_2q: Latency of two qubit gate operations. + gate_time_meas: Latency of readout operations. + t1: T1 coherence time. + t2: T2 coherence time. + """ + + native_gate_set = set(native_gates) + gates_1q = native_gate_set & gate_names.SINGLE_QUBIT_GATE_NAMES + gates_2q = native_gate_set & gate_names.TWO_QUBIT_GATE_NAMES + + if gates_1q | gates_2q != native_gate_set: + raise ValueError( + "Only single and two qubit gates are supported as native gates" + ) + + qubits = list(lattice.qubits) + qubit_count = len(qubits) + qubit_properties = {q: QubitProperty() for q in qubits} + gate_properties = [ + GateProperty( + gate_names.Measurement, + (), + gate_error=gate_error_meas, + gate_time=gate_time_meas, + ) + ] + gate_properties.extend( + [ + GateProperty(name, (), gate_error=gate_error_1q, gate_time=gate_time_1q) + for name in gates_1q + ] + ) + gate_properties.extend( + [ + GateProperty(name, (), gate_error=gate_error_2q, gate_time=gate_time_2q) + for name in gates_2q + ] + ) + + graph = nx.grid_2d_graph(lattice.xsize, lattice.ysize) + mapping = { + (x, y): lattice.get_qubit((x, y)) + for y in range(lattice.ysize) + for x in range(lattice.xsize) + } + graph = nx.relabel_nodes(graph, mapping) + + trans = SequentialTranspiler( + [ + SquareLatticeSWAPInsertionTranspiler(lattice), + GateSetConversionTranspiler(native_gates), + ] + ) + + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=graph, + qubit_properties=qubit_properties, + native_gates=native_gates, + gate_properties=gate_properties, + physical_qubit_count=qubit_count, + # TODO Calculate backgraound error from t1 and t2 + background_error=None, + transpiler=trans, + ) diff --git a/packages/core/quri_parts/backend/devices/star_device.py b/packages/core/quri_parts/backend/devices/star_device.py new file mode 100644 index 00000000..16aed5ee --- /dev/null +++ b/packages/core/quri_parts/backend/devices/star_device.py @@ -0,0 +1,144 @@ +from math import ceil +from typing import cast + +import networkx as nx + +from quri_parts.backend.device import DeviceProperty, GateProperty, QubitProperty +from quri_parts.backend.units import TimeValue +from quri_parts.circuit import gate_names, noise +from quri_parts.circuit.transpile import ( + ParametricPauliRotationDecomposeTranspiler, + ParametricRX2RZHTranspiler, + ParametricRY2RZHTranspiler, + ParametricSequentialTranspiler, + ParametricTranspiler, + STARSetTranspiler, +) + + +def generate_device_property( + qubit_count: int, + code_distance: int, + qec_cycle: TimeValue, + physical_error_rate: float = 0.0, + data_total_qubit_ratio: float = 4.0, +) -> DeviceProperty: + """Generate DeviceInfo object for STAR architecture devices. + + Args: + qubit_count: Number of logical qubits. + physical_error_rate: Error rate of physical qubit operations. + code_distance: Code distance of the quantum error correction code. + qec_cycle: Time duration of each syndrome measurement for quantum + error correction (without code distance dependency). + data_total_qubit_ratio: Ratio of number of total logical qubits to + number of data logical qubits. + + Returns: + DeviceInfo object representing the target STAR architecture device. + + References: + https://arxiv.org/abs/2303.13181 + """ + + # cf. https://arxiv.org/abs/2303.13181 (pp.11-14) + def logical_error_model(ci: float, pthi: float, p: float, d: int) -> float: + return cast(float, ci * (p / pthi) ** ((d + 1.0) / 2.0)) + + def logical_error_round(p: float, d: int) -> float: + plz = logical_error_model(ci=0.067976, pthi=0.0038510, p=p, d=d) + plx = logical_error_model(ci=0.081997, pthi=0.0041612, p=p, d=d) + return plz + plx + + logical_fidelity_round = 1.0 - logical_error_round( + p=physical_error_rate, d=code_distance + ) + + rus_steps = 2 + + qubits = list(range(qubit_count)) + qubit_properties = {q: QubitProperty() for q in qubits} + gate_properties = [ + GateProperty( + gate_names.H, + (), + gate_error=0.0, + gate_time=TimeValue( + value=3.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.S, + [], + gate_error=0.0, + # TODO Confirm latency value + gate_time=TimeValue( + value=2.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.CNOT, + (), + gate_error=0.0, + gate_time=TimeValue( + value=2.0 * code_distance * qec_cycle.value, unit=qec_cycle.unit + ), + ), + GateProperty( + gate_names.RZ, + (), + gate_error=1.0 - (1.0 - 2.0 * physical_error_rate / 15.0) ** rus_steps, + gate_time=TimeValue( + value=2.0 * rus_steps * code_distance * qec_cycle.value, + unit=qec_cycle.unit, + ), + ), + GateProperty( + gate_names.ParametricRZ, + (), + gate_error=1.0 - (1.0 - 2.0 * physical_error_rate / 15.0) ** rus_steps, + gate_time=TimeValue( + value=2.0 * rus_steps * code_distance * qec_cycle.value, + unit=qec_cycle.unit, + ), + ), + ] + qec_fidelity_per_qec_cycle = logical_fidelity_round**data_total_qubit_ratio + + total_patches = ceil(qubit_count * data_total_qubit_ratio) + # 2d^2 physical qubits per single patch, including syndrome measurement qubits + physical_qubit_count = (2 * code_distance**2) * total_patches + + p1 = 1.0 - (1.0 - 2.0 * physical_error_rate / 15.0) ** rus_steps + noise_model = noise.NoiseModel( + [noise.PhaseFlipNoise(error_prob=p1, target_gates=[gate_names.RZ])] + ) + + trans = STARSetTranspiler() + param_trans = ParametricSequentialTranspiler( + [ + ParametricPauliRotationDecomposeTranspiler(), + ParametricRX2RZHTranspiler(), + ParametricRY2RZHTranspiler(), + ParametricTranspiler(trans), + ] + ) + + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=nx.complete_graph(qubit_count), + qubit_properties=qubit_properties, + native_gates=[ + gate_names.H, + gate_names.S, + gate_names.CNOT, + gate_names.RZ, + ], + gate_properties=gate_properties, + physical_qubit_count=physical_qubit_count, + background_error=(1.0 - qec_fidelity_per_qec_cycle, qec_cycle), + transpiler=trans, + parametric_transpiler=param_trans, + noise_model=noise_model, + ) diff --git a/packages/core/quri_parts/backend/units.py b/packages/core/quri_parts/backend/units.py new file mode 100644 index 00000000..4412029e --- /dev/null +++ b/packages/core/quri_parts/backend/units.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from enum import Enum, auto + + +class TimeUnit(Enum): + SECOND = auto() + MILLISECOND = auto() + MICROSECOND = auto() + NANOSECOND = auto() + + def __repr__(self) -> str: + return "<%s.%s>" % (self.__class__.__name__, self.name) + + @staticmethod + def from_str(name: str) -> "TimeUnit": + return _time_name_table[name.lower()] + + def in_ns(self) -> float: + return _ns_table[self] + + +_time_name_table = { + "s": TimeUnit.SECOND, + "ms": TimeUnit.MILLISECOND, + "us": TimeUnit.MICROSECOND, + "µs": TimeUnit.MICROSECOND, + "ns": TimeUnit.NANOSECOND, +} + +_ns_table = { + TimeUnit.SECOND: 1e9, + TimeUnit.MILLISECOND: 1e6, + TimeUnit.MICROSECOND: 1e3, + TimeUnit.NANOSECOND: 1, +} + + +@dataclass(frozen=True) +class TimeValue: + value: float + unit: TimeUnit + + def in_ns(self) -> float: + return self.value * self.unit.in_ns() + + +class FrequencyUnit(Enum): + HZ = auto() + KHZ = auto() + MHZ = auto() + GHZ = auto() + THZ = auto() + + def __repr__(self) -> str: + return "<%s.%s>" % (self.__class__.__name__, self.name) + + @staticmethod + def from_str(name: str) -> "FrequencyUnit": + return _freq_name_table[name.lower()] + + def in_ghz(self) -> float: + return _ghz_table[self] + + +_freq_name_table = { + "hz": FrequencyUnit.HZ, + "khz": FrequencyUnit.KHZ, + "mhz": FrequencyUnit.MHZ, + "ghz": FrequencyUnit.GHZ, + "thz": FrequencyUnit.THZ, +} + +_ghz_table = { + FrequencyUnit.HZ: 1e-9, + FrequencyUnit.KHZ: 1e-6, + FrequencyUnit.MHZ: 1e-3, + FrequencyUnit.GHZ: 1, + FrequencyUnit.THZ: 1e3, +} + + +@dataclass(frozen=True) +class FrequencyValue: + value: float + unit: FrequencyUnit + + def in_ghz(self) -> float: + return self.value * self.unit.in_ghz() diff --git a/packages/core/tests/backend/test_cost_estimator.py b/packages/core/tests/backend/test_cost_estimator.py new file mode 100644 index 00000000..7629ad72 --- /dev/null +++ b/packages/core/tests/backend/test_cost_estimator.py @@ -0,0 +1,148 @@ +import math + +import networkx as nx + +from quri_parts.backend.cost_estimator import ( + estimate_circuit_fidelity, + estimate_circuit_latency, +) +from quri_parts.backend.device import DeviceProperty, GateProperty, QubitProperty +from quri_parts.backend.units import TimeUnit, TimeValue +from quri_parts.circuit import QuantumCircuit, gate_names, gates +from quri_parts.circuit.gate_names import NonParametricGateNameType + + +def _ideal_device() -> DeviceProperty: + qubit_count = 3 + qubits = list(range(qubit_count)) + qubit_properties = {q: QubitProperty() for q in qubits} + native_gates: list[NonParametricGateNameType] = [ + gate_names.H, + gate_names.S, + gate_names.T, + gate_names.CNOT, + gate_names.TOFFOLI, + ] + gate_properties = [ + GateProperty( + name, + (), + gate_error=0.0, + gate_time=TimeValue(value=1.0, unit=TimeUnit.NANOSECOND), + ) + for name in native_gates + ] + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=nx.complete_graph(qubit_count), + qubit_properties=qubit_properties, + native_gates=native_gates, + gate_properties=gate_properties, + ) + + +def _imperfect_device() -> DeviceProperty: + qubit_count = 3 + qubits = list(range(qubit_count)) + qubit_properties = {q: QubitProperty() for q in qubits} + native_gates: list[NonParametricGateNameType] = [ + gate_names.H, + gate_names.S, + gate_names.T, + gate_names.CNOT, + gate_names.TOFFOLI, + ] + gate_properties = [ + GateProperty( + gate_names.H, + (), + gate_error=1.0e-3, + gate_time=TimeValue(value=10.0, unit=TimeUnit.MICROSECOND), + ), + GateProperty( + gate_names.S, + (), + gate_error=2.0e-3, + gate_time=TimeValue(value=20.0, unit=TimeUnit.MICROSECOND), + ), + GateProperty( + gate_names.T, + (), + gate_error=3.0e-3, + gate_time=TimeValue(value=30.0, unit=TimeUnit.MICROSECOND), + ), + GateProperty( + gate_names.CNOT, + (), + gate_error=4.0e-3, + gate_time=TimeValue(value=40.0, unit=TimeUnit.MICROSECOND), + ), + GateProperty( + gate_names.TOFFOLI, + (), + gate_error=5.0e-3, + gate_time=TimeValue(value=50.0, unit=TimeUnit.MICROSECOND), + ), + ] + return DeviceProperty( + qubit_count=qubit_count, + qubits=qubits, + qubit_graph=nx.complete_graph(qubit_count), + qubit_properties=qubit_properties, + native_gates=native_gates, + gate_properties=gate_properties, + ) + + +def _circuit() -> QuantumCircuit: + circuit = QuantumCircuit(3) + circuit.extend( + [ + gates.H(0), + gates.CNOT(0, 1), + gates.S(0), + gates.T(1), + gates.H(2), + gates.TOFFOLI(0, 1, 2), + ] + ) + return circuit + + +def test_estimate_cost_for_empty() -> None: + circuit = QuantumCircuit(3) + + for device in [_ideal_device(), _imperfect_device()]: + assert 0.0 == estimate_circuit_latency(circuit, device).value + assert 1.0 == estimate_circuit_fidelity(circuit, device) + + +def test_estimate_cost_ideal() -> None: + circuit = _circuit() + device = _ideal_device() + + assert math.isclose(4.0, estimate_circuit_latency(circuit, device).value) + assert math.isclose(1.0, estimate_circuit_fidelity(circuit, device)) + + +def test_estimate_cost_real() -> None: + circuit = _circuit() + device = _imperfect_device() + + assert math.isclose(130.0e3, estimate_circuit_latency(circuit, device).value) + assert math.isclose(0.9840996904986060, estimate_circuit_fidelity(circuit, device)) + + +def test_estimate_cost_background() -> None: + circuit = _circuit() + device = _ideal_device() + device.background_error = (1.0e-7, TimeValue(value=1.0, unit=TimeUnit.NANOSECOND)) + assert math.isclose( + 1.0, + estimate_circuit_fidelity(circuit, device, background_error=False), + ) + assert math.isclose( + 0.9999988000006607, + estimate_circuit_fidelity(circuit, device, background_error=True), + ) diff --git a/packages/core/tests/backend/test_devices.py b/packages/core/tests/backend/test_devices.py new file mode 100644 index 00000000..a4088e39 --- /dev/null +++ b/packages/core/tests/backend/test_devices.py @@ -0,0 +1,163 @@ +import math + +import numpy as np + +from quri_parts.backend.cost_estimator import ( + estimate_circuit_fidelity, + estimate_circuit_latency, +) +from quri_parts.backend.devices import ( + clifford_t_device, + nisq_iontrap_device, + nisq_spcond_lattice, + star_device, +) +from quri_parts.backend.units import TimeUnit, TimeValue +from quri_parts.circuit import QuantumCircuit, gate_names, gates +from quri_parts.circuit.topology import SquareLattice + + +def _star_native_circuit() -> QuantumCircuit: + circuit = QuantumCircuit(2) + circuit.extend( + [ + gates.H(0), + gates.CNOT(0, 1), + gates.RZ(1, np.pi / 7.0), + ] + ) + return circuit + + +def _non_star_native_circuit() -> QuantumCircuit: + circuit = QuantumCircuit(2) + circuit.extend( + [ + gates.X(0), + gates.CZ(0, 1), + gates.RY(1, np.pi / 7.0), + ] + ) + return circuit + + +def test_star_device() -> None: + device = star_device.generate_device_property( + qubit_count=16, + code_distance=7, + qec_cycle=TimeValue(value=1.0, unit=TimeUnit.MICROSECOND), + physical_error_rate=1.0e-4, + ) + + assert device.qubit_count == 16 + assert device.physical_qubit_count == 6272 + assert set(device.native_gates) == { + gate_names.H, + gate_names.S, + gate_names.CNOT, + gate_names.RZ, + } + assert device.background_error is not None + error, time = device.background_error + assert math.isclose(2.3302081608722602e-07, error) + assert math.isclose(1.0e3, time.in_ns()) + + circuit = _star_native_circuit() + assert 0.0 < estimate_circuit_fidelity(circuit, device, background_error=True) < 1.0 + assert 0.0 < estimate_circuit_latency(circuit, device).value + + assert device.transpiler is not None + assert { + gate.name for gate in device.transpiler(_non_star_native_circuit()).gates + } <= set(device.native_gates) + + assert device.noise_model is not None + + +def test_clifford_t_device() -> None: + device = clifford_t_device.generate_device_property( + qubit_count=16, + code_distance=7, + qec_cycle=TimeValue(value=1.0, unit=TimeUnit.MICROSECOND), + delta_sk=2.6e-5, + mode_block="intermediate", + physical_error_rate=1.0e-4, + ) + assert device.qubit_count == 16 + assert device.physical_qubit_count == 6762 + assert set(device.native_gates) == { + gate_names.H, + gate_names.S, + gate_names.T, + gate_names.CNOT, + } + assert device.background_error is not None + error, time = device.background_error + assert math.isclose(2.512255650177764e-07, error) + assert math.isclose(1.0e3, time.in_ns()) + + circuit = _star_native_circuit() + assert 0.0 < estimate_circuit_fidelity(circuit, device, background_error=True) < 1.0 + assert 0.0 < estimate_circuit_latency(circuit, device).value + + +def test_nisq_iontrap_device() -> None: + native_gates: set[gate_names.GateNameType] = { + gate_names.RX, + gate_names.RY, + gate_names.RZ, + gate_names.CZ, + } + + device = nisq_iontrap_device.generate_device_property( + qubit_count=16, + native_gates=native_gates, + gate_error_1q=1.0e-5, + gate_error_2q=1.0e-3, + gate_error_meas=1.0e-3, + gate_time_1q=TimeValue(value=100.0, unit=TimeUnit.MICROSECOND), + gate_time_2q=TimeValue(value=500.0, unit=TimeUnit.MICROSECOND), + gate_time_meas=TimeValue(value=1.0, unit=TimeUnit.MILLISECOND), # TODO check + t1=TimeValue(value=10.0, unit=TimeUnit.SECOND), + t2=TimeValue(value=1.0, unit=TimeUnit.SECOND), + ) + assert device.qubit_count == 16 + assert set(device.native_gates) == native_gates + + assert device.transpiler is not None + circuit = device.transpiler(_star_native_circuit()) + assert ( + 0.0 < estimate_circuit_fidelity(circuit, device, background_error=False) < 1.0 + ) + assert 0.0 < estimate_circuit_latency(circuit, device).value + + +def test_nisq_spcond_device() -> None: + native_gates: set[gate_names.GateNameType] = { + gate_names.RX, + gate_names.RY, + gate_names.RZ, + gate_names.CZ, + } + + device = nisq_spcond_lattice.generate_device_property( + lattice=SquareLattice(4, 4), + native_gates=native_gates, + gate_error_1q=1.0e-4, + gate_error_2q=1.0e-2, + gate_error_meas=1.0e-2, + gate_time_1q=TimeValue(value=500.0, unit=TimeUnit.NANOSECOND), + gate_time_2q=TimeValue(value=500.0, unit=TimeUnit.NANOSECOND), + gate_time_meas=TimeValue(value=500.0, unit=TimeUnit.NANOSECOND), # TODO check + t1=TimeValue(value=200.0, unit=TimeUnit.MICROSECOND), + t2=TimeValue(value=100.0, unit=TimeUnit.MICROSECOND), + ) + assert device.qubit_count == 16 + assert set(device.native_gates) == native_gates + + assert device.transpiler is not None + circuit = device.transpiler(_star_native_circuit()) + assert ( + 0.0 < estimate_circuit_fidelity(circuit, device, background_error=False) < 1.0 + ) + assert 0.0 < estimate_circuit_latency(circuit, device).value