"""Cross-platform circuit equivalence assertions.
Assert that circuits expressed in different frameworks (Qiskit, Cirq, pytket)
implement the same unitary up to global phase.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from pytest_quantum.converters.to_unitary import (
_is_cirq,
_is_pytket,
_is_qiskit,
_reverse_qubit_order,
to_unitary,
)
if TYPE_CHECKING:
from numpy.typing import NDArray
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _framework_label(circuit: object, hint: str | None) -> str:
"""Return a human-readable framework name for error messages."""
if hint is not None:
return hint
mod = type(circuit).__module__
if mod.startswith("qiskit"):
return "qiskit"
if mod.startswith("cirq"):
return "cirq"
if mod.startswith("pytket"):
return "pytket"
if mod.startswith("braket"):
return "braket"
if mod.startswith("pennylane"):
return "pennylane"
return mod.split(".")[0]
def _unitaries_equivalent(
u_a: NDArray[np.complex128],
u_b: NDArray[np.complex128],
*,
atol: float,
allow_global_phase: bool,
) -> bool:
"""Return True if u_a and u_b are close (optionally up to global phase)."""
if np.allclose(u_a, u_b, atol=atol):
return True
if not allow_global_phase:
return False
flat_idx = int(np.argmax(np.abs(u_a)))
a_val = u_a.flat[flat_idx]
b_val = u_b.flat[flat_idx]
if abs(a_val) > 1e-10 and abs(b_val) > 1e-10:
phase = a_val / b_val
if np.allclose(u_a, phase * u_b, atol=atol):
return True
return False
def _normalise_endianness(
u_a: NDArray[np.complex128],
circuit_a: object,
u_b: NDArray[np.complex128],
circuit_b: object,
) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]:
"""Align qubit ordering conventions between frameworks.
Qiskit uses little-endian; Cirq and pytket use big-endian.
When comparing across these conventions, reverse one of the unitaries.
"""
big_endian_a = _is_cirq(circuit_a) or _is_pytket(circuit_a)
big_endian_b = _is_cirq(circuit_b) or _is_pytket(circuit_b)
if _is_qiskit(circuit_a) and big_endian_b:
u_a = _reverse_qubit_order(u_a)
elif big_endian_a and _is_qiskit(circuit_b):
u_b = _reverse_qubit_order(u_b)
return u_a, u_b
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
[docs]
def assert_qiskit_cirq_equivalent(
qiskit_circuit: object,
cirq_circuit: object,
*,
atol: float = 1e-6,
allow_global_phase: bool = True,
) -> None:
"""Assert that a Qiskit circuit and a Cirq circuit implement the same unitary.
Convenience wrapper around :func:`assert_cross_platform_equivalent` with
the framework labels pre-set to ``"qiskit"`` and ``"cirq"``.
Qubit-ordering is normalised automatically (Qiskit is little-endian, Cirq
is big-endian), so you can compare circuits using their natural qubit
indices in each framework without manual reversal.
Args:
qiskit_circuit: A ``qiskit.QuantumCircuit`` instance.
cirq_circuit: A ``cirq.Circuit`` instance.
atol: Absolute tolerance for element-wise comparison
(default ``1e-6``).
allow_global_phase: If ``True`` (default), circuits that differ only
by a global phase are considered equivalent.
Raises:
AssertionError: If the circuits implement different unitaries.
ValueError: If either circuit cannot be converted to a unitary.
Example::
import cirq
from qiskit import QuantumCircuit
from pytest_quantum.assertions.cross_platform import (
assert_qiskit_cirq_equivalent,
)
qc = QuantumCircuit(1)
qc.h(0)
q = cirq.LineQubit.range(1)
cc = cirq.Circuit(cirq.H(q[0]))
assert_qiskit_cirq_equivalent(qc, cc)
"""
assert_cross_platform_equivalent(
qiskit_circuit,
cirq_circuit,
atol=atol,
allow_global_phase=allow_global_phase,
framework_a="qiskit",
framework_b="cirq",
)
[docs]
def assert_qiskit_pytket_equivalent(
qiskit_circuit: object,
pytket_circuit: object,
*,
atol: float = 1e-6,
allow_global_phase: bool = True,
) -> None:
"""Assert that a Qiskit circuit and a pytket circuit implement the same unitary.
Convenience wrapper around :func:`assert_cross_platform_equivalent` with
the framework labels pre-set to ``"qiskit"`` and ``"pytket"``.
The pytket unitary is obtained via ``circuit.get_unitary()`` (exposed by
:func:`pytest_quantum.converters.to_unitary.to_unitary`). Qubit-ordering
is normalised automatically (Qiskit is little-endian, pytket is big-endian).
Args:
qiskit_circuit: A ``qiskit.QuantumCircuit`` instance.
pytket_circuit: A ``pytket.Circuit`` instance.
atol: Absolute tolerance for element-wise comparison
(default ``1e-6``).
allow_global_phase: If ``True`` (default), circuits that differ only
by a global phase are considered equivalent.
Raises:
AssertionError: If the circuits implement different unitaries.
ValueError: If either circuit cannot be converted to a unitary.
Example::
from pytket import Circuit as TketCircuit
from qiskit import QuantumCircuit
from pytest_quantum.assertions.cross_platform import (
assert_qiskit_pytket_equivalent,
)
qc = QuantumCircuit(1)
qc.h(0)
tk = TketCircuit(1)
tk.H(0)
assert_qiskit_pytket_equivalent(qc, tk)
"""
assert_cross_platform_equivalent(
qiskit_circuit,
pytket_circuit,
atol=atol,
allow_global_phase=allow_global_phase,
framework_a="qiskit",
framework_b="pytket",
)