Source code for qtest.backends.cirq_backend
"""Cirq-based simulator backend for qtest.
Import-safe without Cirq: every ``import cirq`` is deferred to a method body,
so constructing :class:`CirqBackend` (and importing this module) never requires
Cirq. Calling an execution method does.
Native circuit type
--------------------
This backend operates on :class:`cirq.Circuit` objects over
:class:`cirq.LineQubit` (or any sortable qubit). Measurement bitstrings are
ordered with the **lowest-index qubit leftmost** (Cirq's big-endian
convention), which is the *opposite* of Qiskit's. Keep your ``expected`` dicts
consistent with whichever backend you select.
Noise
-----
qtest's :class:`~qtest.noise.NoiseModel` is built on Qiskit Aer, so passing a
``noise_model`` to this backend raises :class:`NotImplementedError`. Noiseless
simulation is fully supported.
"""
from __future__ import annotations
from typing import Any
import numpy as np
from qtest.backends.base import Backend, CircuitResources
_CIRQ_MISSING_MSG = "CirqBackend requires Cirq. Install it with:\n pip install 'qtest[cirq]'"
_MEASURE_KEY = "qtest_all"
def _require_cirq() -> Any:
try:
import cirq
except ImportError as exc: # pragma: no cover - exercised only without cirq
raise ImportError(_CIRQ_MISSING_MSG) from exc
return cirq
[docs]
class CirqBackend(Backend):
"""Cirq state-vector simulator backend.
Parameters
----------
shots
Default shot count for :meth:`run_circuit`.
"""
def __init__(self, shots: int = 1024) -> None:
if not isinstance(shots, int) or isinstance(shots, bool) or shots <= 0:
raise ValueError(f"shots must be a positive integer, got {shots!r}")
self._default_shots = shots
@property
def name(self) -> str:
return "cirq:simulator"
@property
def supports_statevector(self) -> bool:
return True
# ------------------------------------------------------------------ #
# Execution #
# ------------------------------------------------------------------ #
[docs]
def run_circuit(
self,
circuit: Any,
shots: int | None = None,
seed: int | None = None,
noise_model: Any | None = None,
) -> dict[str, int]:
cirq = _require_cirq()
if circuit is None:
raise ValueError("circuit must not be None")
if noise_model is not None:
raise NotImplementedError(
"CirqBackend does not support qtest noise models (they target "
"Qiskit Aer). Use the Qiskit backend for noisy simulation."
)
shots_val = self._default_shots if shots is None else shots
if not isinstance(shots_val, int) or isinstance(shots_val, bool) or shots_val <= 0:
raise ValueError(f"shots must be a positive integer, got {shots_val!r}")
if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool)):
raise ValueError(f"seed must be an integer or None, got {seed!r}")
qubits = sorted(circuit.all_qubits())
if not qubits:
raise ValueError("circuit has no qubits to measure")
# Rebuild without any existing measurements, then measure all qubits.
ops = [op for moment in circuit for op in moment if not cirq.is_measurement(op)]
sim_circuit = cirq.Circuit(ops)
sim_circuit.append(cirq.measure(*qubits, key=_MEASURE_KEY))
result = cirq.Simulator(seed=seed).run(sim_circuit, repetitions=shots_val)
histogram = result.histogram(key=_MEASURE_KEY)
n = len(qubits)
return {format(int(outcome), f"0{n}b"): int(count) for outcome, count in histogram.items()}
[docs]
def get_statevector(self, circuit: Any) -> np.ndarray:
_require_cirq()
if circuit is None:
raise ValueError("circuit must not be None")
import cirq
final = cirq.Simulator().simulate(circuit).final_state_vector
return np.asarray(final, dtype=complex)
[docs]
def get_unitary(self, circuit: Any) -> np.ndarray:
cirq = _require_cirq()
if circuit is None:
raise ValueError("circuit must not be None")
return np.asarray(cirq.unitary(circuit), dtype=complex)
[docs]
def get_resources(self, circuit: Any) -> CircuitResources:
cirq = _require_cirq()
if circuit is None:
raise ValueError("circuit must not be None")
gate_counts: dict[str, int] = {}
size = 0
two_qubit = 0
multi_qubit = 0
for moment in circuit:
for op in moment:
if cirq.is_measurement(op):
continue
size += 1
# Normalise to lowercase so names line up with Qiskit's
# (e.g. cirq.T -> "t", cirq.H -> "h").
name = str(op.gate).lower()
gate_counts[name] = gate_counts.get(name, 0) + 1
n = len(op.qubits)
if n >= 2:
multi_qubit += 1
if n == 2:
two_qubit += 1
return CircuitResources(
num_qubits=len(circuit.all_qubits()),
depth=len(circuit),
size=size,
gate_counts=gate_counts,
two_qubit_count=two_qubit,
multi_qubit_count=multi_qubit,
)