Source code for qolumbina.utils.execution_framework

r"""
The top-level interface for quantum system simulation.
The current version has the following limitations:

1.
    Only the quantum programs executing a unitary operation :math:`U`
    are supported. This means that the circuit under test
    cannot include classical bits or mid-circuit measurements.
2.
    Parameterized quantum gates are not supported for state initialization,
    such as :math:`R_X(\theta)` being excluded.
3.
    The measurement basis is currently limited to Pauli bases (:math:`X`,
    :math:`Y`, :math:`Z`) or identity (:math:`I`, meaning no measurement).

These constraints are planned to be relaxed in the future development.
"""


from qiskit import QuantumCircuit, transpile
from typing import Callable, Optional, Any
from qiskit_aer.noise import NoiseModel
from typing import Literal
from qiskit.converters import circuit_to_dag
import math

from . import (
    PureStateInitialization, MixedStateInitialization, QuantumMeasurement,
    ShotBasedBackend, ShotIndependentBackend
)

from qolumbina.utils.data_conversion import decompose_joint_state

[docs] class QuantumSystemSimulation: r""" Execute a quantum program on a shot-based simulated quantum computer. This class builds an execution circuit around a circuit under test: 1. prepare an input state on selected system qubits; 2. compose the quantum circuit returned by ``tested_subroutine``; 3. apply Pauli-basis measurements; 4. execute the complete circuit on the selected backend. The circuit returned by ``tested_subroutine`` defines the system-qubit register. In pure-state mode, ``init_qubits`` may select only a subset of those system qubits for explicit input-state preparation; all other system qubits remain in :math:`\ket{0}`. In mixed-state mode, the selected system qubits are prepared through appended control/environment qubits, while the circuit under test still acts only on the original system qubits. """ def __init__( self, tested_subroutine: Callable[[], QuantumCircuit], init_state: list[str] | list[float] | list[complex] | None = None, init_qubits: Literal["all"] | list[int] | None = "all", input_mode: Literal["pure", "mixed"] = "pure", meas_mapping: dict[int, int] | None = None, meas_basis: list[str] | dict[int, str] | None = None, shots: int = 1024, backend: str = "ideal", noise_model: Optional[NoiseModel] = None, if_noisy: bool = False, random_seed: int | None = None, mixed_state_angles: list[float] | None = None ) -> None: r""" Args: tested_subroutine: A callable that constructs and returns the `QuantumCircuit` under test. init_state: Initial state specification for the selected input qubits. If ``None``, the selected input qubits are initialized to :math:`\ket{0}`. In pure-state mode, the meaning of ``init_state`` depends on ``init_qubits``. If ``init_qubits="all"``, ``init_state`` describes every system qubit. If ``init_qubits`` is a list, ``init_state`` describes only those listed qubits, matched by position. For example, for a 5-qubit circuit, using ``init_state=["x", "x"]`` and ``init_qubits=[0, 1]`` prepares only system qubits ``q0`` and ``q1`` as :math:`\ket{11}`; the remaining system qubits ``q2``, ``q3``, and ``q4`` remain :math:`\ket{0}` unless changed by the circuit under test. See Also: :doc:`qolumbina.utils.state_initialization` for input types for state initialization. init_qubits: Specifies which system qubits need input-state preparation. The default value is `"all"`. Use ``None`` to skip explicit input-state preparation; this is only valid when ``init_state`` is also ``None``. - ``"all"``: Pure mode: all qubits are initialized according to ``init_state``. Mixed mode: if ``mixed_state_angles`` is provided, the targets default to the low ``m`` system qubits, where ``m`` is the number of angles; otherwise all system qubits are mixed-state targets. - ``list[int]``: Pure mode: only these qubits are initialized according to ``init_state``. Mixed mode: these are the system target qubits for mixed-state preparation. - ``None``: No qubits are explicitly initialized. The system qubits keep the default :math:`\ket{0}` state. This cannot be combined with ``init_state``. Example: ``init_qubits=[0, 2]`` means that only qubits with indices 0 and 2 are prepared, while other system qubits remain initialized to :math:`\ket{0}` by default. input_mode: Input-state preparation mode. ``"pure"`` preserves the original ``init_state`` / ``init_qubits`` behavior. ``"mixed"`` uses quantum-control qubits and ``mixed_state_angles`` to prepare separable mixed states on the system qubits selected by ``init_qubits``. meas_mapping: Measurement mapping from qubit index to classical bit index, which impacts the analysis of test output. Default value is ``None``. If both ``meas_mapping`` and ``meas_basis`` are ``None``, every default measured system qubit is measured in Pauli-Z basis and mapped correspondingly, meaning that qubit with index :math:`i` is measured to classical bit with index :math:`i`. If ``meas_mapping`` is provided while ``meas_basis`` is ``None``, only the qubits in ``meas_mapping`` are measured, all in Pauli-Z basis. In mixed-state mode, appended control/environment qubits are internal to input preparation and are not included in this default output mapping. Example: ``meas_mapping={0: 1, 2: 0}`` means that qubit 0 is measured to classical bit 1, while qubit 2 is measured to classical bit 0. See Also: :class:`qolumbina.utils.QuantumMeasurement` and :meth:`qolumbina.utils.QuantumMeasurement.measure` for the complete measurement-resolution rules and examples that control ``meas_mapping`` independently from ``meas_basis``. meas_basis: Measurement basis. Can be ``"x"``, ``"y"``, ``"z"`` or ``"i"`` (identity, meaning no measurement). A ``list[str]`` is dense and must cover all qubits in the circuit being measured. A ``dict[int, str]`` is sparse: its keys are qubit indices and unspecified qubits are treated as ``"i"``. Default value is ``None``. If both ``meas_basis`` and ``meas_mapping`` are ``None``, all default measured system qubits are measured in Pauli-Z basis. In pure-state mode this means every system qubit in the circuit under test. In mixed-state mode this means every original system qubit, not the appended control/environment qubits. Example: ``meas_basis=["x", "z", "y"]`` means that qubit 0 is measured in Pauli-X basis, qubit 1 is measured in Pauli-Z basis, and qubit 2 is measured in Pauli-Y basis. To measure only a subset of qubits, either use ``"i"`` for qubits that should not be measured, or provide a sparse dict. When ``meas_mapping`` is omitted, classical bits are assigned contiguously in ascending qubit-index order. For example, for a 5-qubit circuit, ``meas_basis=["z", "z", "i", "i", "i"]`` or ``meas_basis={0: "z", 1: "z"}`` measures only ``q0`` and ``q1`` and maps them to ``c0`` and ``c1``. If both ``meas_basis`` and ``meas_mapping`` are provided, their measured qubit sets must match exactly; otherwise a ``ValueError`` is raised. See Also: :class:`qolumbina.utils.QuantumMeasurement` and :meth:`qolumbina.utils.QuantumMeasurement.measure` for the complete measurement-resolution rules and examples that control ``meas_basis`` independently from ``meas_mapping``. shots: Number of shots per circuit execution. Default value is 1024. backend: Backend name for circuit execution. See Also: :doc:`qolumbina.utils.backend_execution` for supported backends. noise_model: Optional custom NoiseModel. if_noisy: Whether to enable noise when using fake backend. random_seed: Optional random seed for simulator to ensure reproducibility. mixed_state_angles: Optional Ry angles for separable quantum-control qubits used to prepare a mixed-state input when ``input_mode="mixed"``. Each angle prepares one extra control qubit as ``Ry(theta)|0>`` and then controls one target system qubit. If ``None`` in mixed mode, all target angles default to ``pi / 2``, which is equivalent to Hadamard probabilities. In mixed-state mode, system qubits keep their original indices ``0..n-1``. Target qubits are selected by ``init_qubits`` and can be any distinct valid system-qubit indices. Control qubits are appended after the maximum system-qubit index, namely ``n..n+m-1``. Control qubits are not measured into the test output; they only act as an environment/purification for the target system qubits. Example: To prepare the single-qubit mixed state :math:`\rho = \cos^2(\theta / 2)\lvert 0\rangle\langle 0\rvert + \sin^2(\theta / 2)\lvert 1\rangle\langle 1\rvert` on system qubit ``0``, use one appended control qubit prepared by ``Ry(theta)`` and entangled with the target: .. code-block:: python sim = QuantumSystemSimulation( tested_subroutine=lambda: QuantumCircuit(1), input_mode="mixed", mixed_state_angles=[theta], init_qubits=[0], shots=1024, backend="ideal", ) For a two-qubit separable mixed input such as :math:`\rho = \rho_0 \otimes \rho_2` on non-contiguous system qubits ``0`` and ``2``, provide one angle per target qubit: .. code-block:: python sim = QuantumSystemSimulation( tested_subroutine=lambda: QuantumCircuit(3), input_mode="mixed", mixed_state_angles=[theta_0, theta_2], init_qubits=[0, 2], shots=1024, backend="ideal", ) .. rubric:: Mode-specific defaults ``input_mode="pure"`` Pure-state mode is the default mode. The input-related defaults are: - ``init_state=None``: initialize the selected system qubits to :math:`\ket{0}`. - ``init_qubits="all"``: initialize every system qubit according to ``init_state``. - ``init_qubits=None``: skip explicit input-state preparation; all system qubits remain in their default :math:`\ket{0}` state. - ``mixed_state_angles=None``: no mixed-state preparation is used. In this mode, ``mixed_state_angles`` must remain ``None``. The resolved prepared system qubits are stored in ``self.input_preparation_qubits``. ``input_mode="mixed"`` Mixed-state mode prepares selected system qubits through appended quantum-control qubits. The input-related defaults are: - ``init_state=None``: required. System targets are prepared by mixed-state preparation, not by the pure-state initializer. - ``init_qubits="all"`` and ``mixed_state_angles=None``: all system qubits are mixed-state targets and all angles default to ``pi / 2``. - ``init_qubits="all"`` and ``mixed_state_angles`` is a list of length ``m``: the mixed-state targets default to low-index system qubits ``[0, 1, ..., m - 1]``. - ``init_qubits`` is ``list[int]`` and ``mixed_state_angles=None``: the listed qubits are mixed-state targets and all angles default to ``pi / 2``. - ``init_qubits`` is ``list[int]`` and ``mixed_state_angles`` is a list: the two lists are matched position-wise, so ``mixed_state_angles[i]`` prepares ``init_qubits[i]``. In this mode, the resolved target system qubits are stored in ``self.input_preparation_qubits`` and the appended control qubits are stored in ``self.mixed_state_control_qubits``. Control qubits are not measured into ``self.test_output``. .. rubric:: Measurement output and bit-string order ``self.test_output`` stores the shot counts returned by Qiskit, so the keys follow Qiskit's classical-bit string convention. The leftmost character corresponds to the highest displayed classical bit and the rightmost character corresponds to classical bit 0. With the default measurement mapping ``q_i -> c_i``, this means that a 5-qubit circuit whose measured state is ``q4 q3 q2 q1 q0 = 0 0 0 1 1`` appears in ``self.test_output`` as ``"00011"``, not ``"11000"``. This convention matters when writing expected outputs for tests: the bit string is displayed in classical register order, while qubit indices are commonly discussed from low to high. Example: .. code-block:: python sim = QuantumSystemSimulation( tested_subroutine=lambda: QuantumCircuit(5), init_state=["x", "x"], init_qubits=[0, 1], meas_basis=None, shots=1024, backend="ideal", ) # Empty circuit under test; q0 and q1 are one, q2..q4 are zero. # The expected count key is "00011" under the default q_i -> c_i # measurement mapping. assert sim.test_output == {"00011": 1024} If only low-index qubits are relevant, measure only those qubits to make the output width explicit: .. code-block:: python sim = QuantumSystemSimulation( tested_subroutine=lambda: QuantumCircuit(5), init_state=["x", "x"], init_qubits=[0, 1], meas_basis=["z", "z", "i", "i", "i"], meas_mapping={0: 0, 1: 1}, shots=1024, backend="ideal", ) assert sim.test_output == {"11": 1024} .. rubric:: Input validation and errors The following cases are checked before or during input-state preparation. They are grouped by configuration area so callers can map an exception back to the invalid input quickly. - Mode selection: ``ValueError`` is raised if ``input_mode`` is not ``"pure"`` or ``"mixed"``. - Explicitly skipping initialization: ``init_qubits=None`` means no qubits are explicitly initialized. It is valid only when ``init_state is None`` and ``mixed_state_angles is None``. Otherwise ``ValueError`` is raised, because those inputs would have no target qubits to initialize. - Pure-state mode: ``mixed_state_angles`` is invalid when ``input_mode="pure"`` and raises ``ValueError``. If pure-state preparation is used, the delegated ``PureStateInitialization`` validation may also raise: ``TypeError`` when ``init_state`` is not a supported list or ``init_qubits`` is neither ``"all"`` nor ``list[int]``; ``ValueError`` when ``init_state`` is empty, contains an undefined single-qubit gate, has a gate-list length that does not match ``init_qubits``, has a state-vector dimension that does not match the selected qubits, or violates the generated circuit-width constraints. - Mixed-state mode: ``init_state`` is invalid when ``input_mode="mixed"`` and raises ``ValueError``. Mixed-state target resolution may also raise: ``ValueError`` when ``mixed_state_angles`` is provided but is not a non-empty list, when ``init_qubits`` is an empty list, when the angle list and explicit target-qubit list have different lengths, when target qubits are duplicated, or when any target qubit is outside the system-qubit range; ``TypeError`` when ``mixed_state_angles`` contains non-real values or ``init_qubits`` contains non-integer values. .. note:: Regarding the test output, we can access the following attributes after execution: self.entire_qc (QuantumCircuit) The full QuantumCircuit executed (including initialization, tested subroutine, measurement). self.transpiled_qc (QuantumCircuit) The transpiled circuit under test, excluding state initialization and measurement. In pure-state mode, explicit state-preparation gates are excluded. In mixed-state mode, this is the system circuit only. The following properties can be accessed for the transpiled circuit: :attr:`transpiled_cut_width`, :attr:`transpiled_cut_size`, :attr:`transpiled_cut_depth`. self.transpiled_execution_qc (QuantumCircuit) The full transpiled QuantumCircuit executed on the backend, including initialization, tested subroutine, control qubits for mixed-state preparation, and measurement operations. The following properties can be accessed for the full execution circuit: :attr:`transpiled_execution_width`, :attr:`transpiled_execution_size`, :attr:`transpiled_execution_depth`. self.test_output (dict[str, int]) Measurement counts as a dictionary. e.g., ``{'00': 512, '11': 512}`` """ # Fixed attributes self._tested_subroutine = tested_subroutine self._init_state = init_state self._init_qubits = init_qubits self._input_mode = input_mode self._mixed_state_angles = mixed_state_angles self._meas_mapping = meas_mapping self._meas_basis = meas_basis self._shots = shots self._backend = backend self._noise_model = noise_model self._noisy = if_noisy self._random_seed = random_seed # init_qubits=None means no explicit qubit initialization; any # init_state value would be ignored, so reject that combination. if self._init_qubits is None and self._init_state is not None: raise ValueError("init_state must be None when init_qubits is None") if self._init_qubits is None and self._mixed_state_angles is not None: raise ValueError("mixed_state_angles must be None when init_qubits is None") # To be set during execution self.num_total_qubits: int = 0 self.num_system_qubits: int = 0 self.num_control_qubits: int = 0 self.num_total_clbits: int = 0 # Resolved system qubits that receive input-state preparation. In pure # mode this mirrors init_qubits after StateInitialization resolves it; # in mixed mode it stores the mixed-state targets derived from # init_qubits and mixed_state_angles. self.input_preparation_qubits: list[int] = [] self.mixed_state_control_qubits: list[int] = [] self.entire_qc: QuantumCircuit | None = None self.circuit_under_test: QuantumCircuit | None = None self.transpiled_qc: QuantumCircuit | None = None self.transpiled_execution_qc: QuantumCircuit | None = None # Run the simulation self.test_output = self._run() @property def init_state(self) -> list: if self._init_state == None: # Default |0> state return ["i"] * self.num_total_qubits else: return self._init_state @property def init_qubits(self) -> Literal["all"] | list[int] | None: # init_qubits=None explicitly disables input-state preparation. if self._init_qubits is None: return None elif self._init_qubits != "all" and not isinstance(self._init_qubits, list): return "all" else: return self._init_qubits @property def meas_basis(self) -> list[str] | dict[int, str]: if self._meas_basis == None: # Default pauli-z measurement return ["z"] * self._default_measured_qubit_count() else: return self._meas_basis @property def meas_mapping(self) -> dict[int, int]: measurement_basis = ( self._measurement_basis_for_clbit_allocation() if self.has_mixed_state_configuration else self._meas_basis ) _, resolved_mapping = QuantumMeasurement.resolve_measurement_spec( self._default_measured_qubit_count(), measurement_basis, self._meas_mapping ) return resolved_mapping @property def has_mixed_state_configuration(self) -> bool: return self._input_mode == "mixed" @property def transpiled_cut_width(self) -> int: """ Number of qubits (a.k.a. width) of the transpiled circuit under test. Compared to ``qc.width`` for ``qc`` with ``QuantumCircuit`` type, classical bits are excluded. """ if self.transpiled_qc is None: return 0 return self.transpiled_qc.num_qubits @property def transpiled_cut_size(self) -> int: """ Number of quantum gates (a.k.a. size) of the transpiled circuit under test. Compared to ``qc.size`` for ``qc`` with ``QuantumCircuit`` type, only quantum gates are counted, meaning that measurement and barrier are excluded. """ if self.transpiled_qc is None: return 0 return self._quantum_gate_size(self.transpiled_qc) @property def transpiled_cut_depth(self) -> int: """ Depth of the quantum-gate DAG (a.k.a. depth) of the transpiled circuit under test. Compared to ``qc.depth`` for ``qc`` with ``QuantumCircuit`` type, only quantum gates are counted for the depth calculation, which should exclude measurement and barrier. """ if self.transpiled_qc is None: return 0 return self._quantum_gate_depth(self.transpiled_qc) @property def transpiled_execution_width(self) -> int: """ Number of qubits of the full transpiled execution circuit. In mixed-state mode, this includes both system qubits and appended control/environment qubits. """ if self.transpiled_execution_qc is None: return 0 return self.transpiled_execution_qc.num_qubits @property def transpiled_execution_size(self) -> int: """ Number of quantum gates of the full transpiled execution circuit. Measurement and barrier operations are excluded, but initialization, mixed-state preparation, basis-change gates, and the circuit under test are included. """ if self.transpiled_execution_qc is None: return 0 return self._quantum_gate_size(self.transpiled_execution_qc) @property def transpiled_execution_depth(self) -> int: """ Quantum-gate depth of the full transpiled execution circuit. Measurement and barrier operations are excluded, but initialization, mixed-state preparation, basis-change gates, and the circuit under test are included. """ if self.transpiled_execution_qc is None: return 0 return self._quantum_gate_depth(self.transpiled_execution_qc) @staticmethod def _quantum_gate_size(qc: QuantumCircuit) -> int: return sum( count for name, count in qc.count_ops().items() if name not in {"measure", "barrier"} ) @staticmethod def _quantum_gate_depth(qc: QuantumCircuit) -> int: dag = circuit_to_dag(qc) quantum_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): if node.op.name not in {"measure", "barrier"}: quantum_dag.apply_operation_back( node.op, node.qargs, [] ) return quantum_dag.depth() def _default_measured_qubit_count(self) -> int: # In mixed-state mode, measurement output should describe only the # original system under test. The appended control qubits are internal # purification qubits and must not appear in test_output bitstrings. if self.has_mixed_state_configuration: return self.num_system_qubits return self.num_total_qubits def _measurement_basis_for_clbit_allocation(self) -> list[str] | dict[int, str] | None: # Classical bits are allocated before mixed-state control qubits are # appended. For mixed mode, compute the output-register width from the # system-qubit portion only, then validate any control-qubit basis after # the initializer has established the final physical width. if not self.has_mixed_state_configuration: return self._meas_basis if self._meas_basis is None: if self._meas_mapping is None: return ["z"] * self.num_system_qubits return None if isinstance(self._meas_basis, dict): return { qubit: basis for qubit, basis in self._meas_basis.items() if qubit < self.num_system_qubits } if len(self._meas_basis) < self.num_system_qubits: raise ValueError( f"the length of measurement basis ({len(self._meas_basis)}) must be at least " f"the number of system qubits ({self.num_system_qubits})" ) return self._meas_basis[:self.num_system_qubits] def _validate_mixed_state_measurement_mapping(self) -> None: if self._meas_mapping is None: return non_system_qubits = [ qubit for qubit in self._meas_mapping if qubit >= self.num_system_qubits ] if non_system_qubits: raise ValueError( "currently, mixed-state control qubits are not measured; " f"measurement mapping includes non-system qubits {non_system_qubits}" ) def _mixed_state_measurement_basis(self) -> list[str] | dict[int, str] | None: if self._meas_basis is None: if self._meas_mapping is None: return ["z"] * self.num_system_qubits + ["i"] * self.num_control_qubits return None system_basis = self._meas_basis if isinstance(system_basis, dict): invalid_controls = [ qubit for qubit, basis in system_basis.items() if qubit >= self.num_system_qubits and basis.lower() != "i" ] if invalid_controls: raise ValueError( "currently, mixed-state control qubits are not measured; " f"non-identity bases were specified for control qubits {invalid_controls}" ) return { qubit: basis for qubit, basis in system_basis.items() if qubit < self.num_system_qubits } if len(system_basis) == self.num_system_qubits: # Let callers specify basis only for the system. The appended control # qubits are automatically marked with identity basis so that # QuantumMeasurement sees a basis entry for every physical qubit but # emits measurements only for the system qubits. return system_basis + ["i"] * self.num_control_qubits if len(system_basis) == self.num_total_qubits: control_basis = system_basis[self.num_system_qubits:] if any(basis.lower() != "i" for basis in control_basis): raise ValueError( "currently, mixed-state control qubits are not measured; " "use 'i' for all control-qubit measurement bases" ) return system_basis raise ValueError( f"the length of measurement basis ({len(system_basis)}) must match " f"the number of system qubits ({self.num_system_qubits})" ) def _run(self) -> dict[str, int]: if self._input_mode not in {"pure", "mixed"}: raise ValueError("input_mode must be either 'pure' or 'mixed'") if self._input_mode == "pure" and self._mixed_state_angles is not None: raise ValueError( "mixed_state_angles can only be used with input_mode='mixed'" ) # Construct the circuit under test tested_qc = self._tested_subroutine() self.num_system_qubits = tested_qc.num_qubits self.num_total_qubits = tested_qc.num_qubits self._validate_mixed_state_measurement_mapping() measurement_basis_for_allocation = self._measurement_basis_for_clbit_allocation() self.num_total_clbits = QuantumMeasurement.required_clbits( self._default_measured_qubit_count(), measurement_basis_for_allocation, self._meas_mapping ) # State initialization if self.init_qubits is None: # init_qubits=None: do not initialize any qubit; keep the # system in Qiskit's default |0...0> state. self.input_preparation_qubits = [] self.entire_qc = QuantumCircuit( self.num_total_qubits, self.num_total_clbits ) elif self.has_mixed_state_configuration: # Mixed-state preparation is an alternative input-state path. # init_qubits is still meaningful here: it selects the system target # qubits. init_state is not meaningful in mixed mode, because the # system targets are initialized through control/environment qubits. if self._init_state is not None: raise ValueError( "init_state cannot be combined with input_mode='mixed'" ) initializer = MixedStateInitialization( system_qubit_count=tested_qc.num_qubits, target_qubits=self.init_qubits, mixed_state_angles=self._mixed_state_angles, predefined_clbit_count=self.num_total_clbits ) self.num_control_qubits = initializer.num_control_qubits self.input_preparation_qubits = initializer.target_qubits self.mixed_state_control_qubits = initializer.control_qubits self.num_total_qubits = initializer.num_total_qubits self.entire_qc = initializer.qc else: # Pure-state mode: preserve the original behavior exactly. self.input_preparation_qubits = ( list(range(self.num_system_qubits)) if self.init_qubits == "all" else self.init_qubits ) self.entire_qc = PureStateInitialization( init_state=self.init_state, init_qubits=self.init_qubits, predefined_qubit_count=self.num_total_qubits, predefined_clbit_count=self.num_total_clbits ).qc # Constraints by this version, where some could be removed in the future development if not self.has_mixed_state_configuration and tested_qc.num_qubits != self.entire_qc.num_qubits: raise ValueError( f"qubit count mismatch between the circuit under test and the main circuit: " f"tested has {tested_qc.num_qubits} qubits, main has {self.entire_qc.num_qubits}" ) if tested_qc.num_clbits > 0: raise ValueError( f"currently, classical bits of the circuit under test are unallowed: " \ f"{tested_qc.num_clbits} bits are included." ) # Compose the circuit under test if self.has_mixed_state_configuration: # The tested subroutine acts only on the original system qubits. # Control qubits are high-index environment qubits and are not part # of the program under test. self.entire_qc.compose( tested_qc, qubits=list(range(self.num_system_qubits)), inplace=True ) else: self.entire_qc.compose(tested_qc, inplace=True) # Quantum measurement meas_basis = ( self._mixed_state_measurement_basis() if self.has_mixed_state_configuration else self._meas_basis ) self.entire_qc = QuantumMeasurement.measure(self.entire_qc, meas_basis, self._meas_mapping) # Run the circuit on backend executable_backend = ShotBasedBackend( self.entire_qc, self._backend, self._noise_model, self._shots, self._noisy, self._random_seed ) # Obtain both the circuit-under-test and full-execution complexity # views. The former excludes initialization/measurement and, in # pure-state mode, excludes explicit state-preparation gates. In # mixed-state mode, it also excludes control/environment qubits. self.circuit_under_test = tested_qc self.transpiled_qc = transpile( tested_qc, executable_backend.backend, optimization_level=0 ) self.transpiled_execution_qc = executable_backend.transpiled_cut # Extract measurement counts test_output = executable_backend.output return test_output
[docs] class AnalyticIdealSimulation: r""" Execute a quantum program on an analytic backend without quantum measurements. """ def __init__( self, tested_subroutine: Callable[[], QuantumCircuit], init_state: list | None = None, init_qubits: Literal["all"] | list[int] | None = "all", backend: str = "statevector", target_qubits: list[int] | None = None ) -> None: r""" Args: tested_subroutine: A callable that constructs and returns the `QuantumCircuit` under test. init_state: Initial state specification for all qubits. If `None`, all qubits are initialized to :math:`\ket{0}`. See Also: :doc:`qolumbina.utils.state_initialization` for input types for state initialization. init_qubits: Specifies which qubits to allocate and initialize according to `init_state`. The default value is `"all"`. Use ``None`` to skip explicit initialization entirely; this is only valid when ``init_state`` is also ``None``. - ``"all"``: All qubits are initialized. This means that the number of qubits for the entire circuit is determined by `init_state`. - ``list[int]``: Only the qubits with the specified indices are initialized according to `init_state`. In this case, the number of qubits for the entire circuit automatically matches that of the circuit under test. - ``None``: No qubits are explicitly initialized. The system qubits keep the default :math:`\ket{0}` state. This cannot be combined with ``init_state``. Example: ``init_qubits=[0, 2]`` means that only qubits with indices 0 and 2 are initialized according to ``init_state``, while other qubits are initialized to :math:`\ket{0}` by default. backend: Backend name for circuit execution. Supported analytic backends include ``"statevector"``, ``"density_matrix"``, and ``"unitary"``. target_qubits: Optional list of qubit indices for which the analytic result is extracted. This is only applicable for the state vector backend. If `None`, the full state vector is returned. .. rubric:: Input validation and errors The following cases are checked before or during input-state preparation. - Explicitly skipping initialization: ``init_qubits=None`` means no qubits are explicitly initialized. It is valid only when ``init_state is None``; otherwise ``ValueError`` is raised, because ``init_state`` would have no target qubits to initialize. - Pure-state initialization: When ``init_qubits`` is ``"all"`` or ``list[int]``, validation is delegated to ``PureStateInitialization``. It may raise ``TypeError`` when ``init_state`` is not a supported list or ``init_qubits`` is neither ``"all"`` nor ``list[int]``. It may raise ``ValueError`` when ``init_state`` is empty, contains an undefined single-qubit gate, has a gate-list length that does not match ``init_qubits``, has a state-vector dimension that does not match the selected qubits, or violates the generated circuit-width constraints. .. note:: Regarding the test output, we can access the following attributes after execution: self.entire_qc (QuantumCircuit) The full QuantumCircuit executed (including initialization, tested subroutine). self.test_output (Any) An analytic result, which could be a state vector, density matrix, or unitary matrix. """ # Fixed attributes self._tested_subroutine = tested_subroutine self._init_state = init_state self._init_qubits = init_qubits self._backend = backend self._target_qubits = target_qubits self._dec_state_type = None # init_qubits=None means no explicit qubit initialization; any # init_state value would be ignored, so reject that combination. if self._init_qubits is None and self._init_state is not None: raise ValueError("init_state must be None when init_qubits is None") # To be set during execution self.num_total_qubits: int = 0 self.entire_qc: QuantumCircuit | None = None # Run the simulation test_output = self._run() if self._target_qubits is not None and self._backend == "statevector": self._dec_state_type, self.test_output = decompose_joint_state( test_output, self._target_qubits ) else: self.test_output = test_output @property def init_state(self) -> list: if self._init_state is None: # Default |0> state return ["i"] * self.num_total_qubits else: return self._init_state @property def init_qubits(self) -> Literal["all"] | list[int] | None: # init_qubits=None explicitly disables input-state preparation. if self._init_qubits is None: return None if self._init_qubits != "all" and not isinstance(self._init_qubits, list): return "all" else: return self._init_qubits def _run(self) -> Any: # Construct the circuit under test tested_qc = self._tested_subroutine() self.num_total_qubits = tested_qc.num_qubits # State initialization, where no classical bits are needed if self.init_qubits is None: # init_qubits=None: do not initialize any qubit; keep the # system in Qiskit's default |0...0> state. self.entire_qc = QuantumCircuit(self.num_total_qubits, 0) else: self.entire_qc = PureStateInitialization( init_state=self.init_state, init_qubits=self.init_qubits, predefined_qubit_count=self.num_total_qubits, predefined_clbit_count=0 ).qc # Compose the circuit under test self.entire_qc.compose(tested_qc, inplace=True) # Run the circuit on backend executable_backend = ShotIndependentBackend( self.entire_qc, self._backend ) analytic_result = executable_backend.output return analytic_result