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