"""********************************************************************************
* Copyright (c) 2026 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************
"""
from __future__ import annotations
import functools
from collections.abc import Callable
from typing import Any
from qrisp.circuit.quantum_circuit import QuantumCircuit
[docs]
class CircuitPass:
"""A decorator for quantum circuit transformation passes.
``CircuitPass`` wraps a callable with signature
``QuantumCircuit -> QuantumCircuit`` and enforces type safety by checking
that both the argument and the return value are :class:`~qrisp.QuantumCircuit`
instances.
It can be used as a bare decorator on a pass function, or instantiated
explicitly with a function reference. Either way, the resulting
``CircuitPass`` object is itself callable and forwards calls to the
wrapped function after performing the type checks.
Examples
--------
Using ``CircuitPass`` as a decorator::
@CircuitPass
def my_pass(qc):
# transform qc
return qc
Wrapping an existing function explicitly::
from qrisp import CircuitPass, fuse_adjacents
typed_pass = CircuitPass(fuse_adjacents)
result = typed_pass(qc)
Inside factory functions that return configured passes::
def convert_to_cz(strict=False):
@CircuitPass
def _convert_to_cz(qc):
...
return _convert_to_cz
"""
def __init__(self, func: Callable[[QuantumCircuit], QuantumCircuit]) -> None:
"""Parameters
----------
func : Callable[[QuantumCircuit], QuantumCircuit]
The transformation function to wrap. The function's ``__name__``
and ``__doc__`` are copied onto the ``CircuitPass`` instance.
"""
self._func: Callable[[QuantumCircuit], QuantumCircuit] = func
functools.update_wrapper(self, func, assigned=("__module__", "__doc__", "__annotations__"))
self.__wrapped__ = func
self.__name__ = func.__name__
def __call__(self, *args: Any, **kwargs: Any) -> Any:
"""Invoke the wrapped pass function with type guards.
The argument must be a :class:`~qrisp.QuantumCircuit`. The wrapped
function receives it and the return value is checked to also be a
:class:`~qrisp.QuantumCircuit`.
Parameters
----------
qc : QuantumCircuit
The quantum circuit to transform.
Returns
-------
QuantumCircuit
The transformed quantum circuit.
Raises
------
TypeError
If the argument is not a :class:`~qrisp.QuantumCircuit`, or if the
wrapped function does not return a :class:`~qrisp.QuantumCircuit`.
"""
# --- Type guard on input ---
if len(args) != 1 or kwargs:
raise TypeError(
f"CircuitPass expected exactly one positional argument "
f"(the QuantumCircuit), got {len(args)} positional and "
f"{len(kwargs)} keyword arguments."
)
qc = args[0]
if not isinstance(qc, QuantumCircuit):
raise TypeError(f"CircuitPass expected a QuantumCircuit as input, got {type(qc).__name__}.")
# --- Forward to the wrapped function ---
result = self._func(qc)
# --- Type guard on output ---
if not isinstance(result, QuantumCircuit):
raise TypeError(
f"CircuitPass expected the wrapped function to return a "
f"QuantumCircuit, but it returned {type(result).__name__}."
)
return result
[docs]
def compare_unitary(
self,
qc: QuantumCircuit,
precision: int = 4,
ignore_gphase: bool = False,
) -> bool:
"""Verify that this :class:`CircuitPass` leaves the unitary invariant
when applied to the given :class:`~qrisp.QuantumCircuit`.
The method copies *qc*, applies the pass to the copy, and then
compares the original and transformed unitaries using
:meth:`QuantumCircuit.compare_unitary`.
Measurements are not allowed because they break unitarity and would
cause the underlying unitary computation to fail.
Parameters
----------
qc : QuantumCircuit
The input quantum circuit to test the pass against.
precision : int, optional
The precision passed to
:meth:`QuantumCircuit.compare_unitary`. The default is 4.
ignore_gphase : bool, optional
If ``True``, ignore global phase differences. The default is
``False``.
Returns
-------
bool
``True`` if the pass preserves the unitary up to the given
precision, ``False`` otherwise.
Raises
------
TypeError
If *qc* is not a :class:`~qrisp.QuantumCircuit`.
ValueError
If either the input or the transformed circuit contains
measurement instructions.
"""
if not isinstance(qc, QuantumCircuit):
raise TypeError(f"Expected a QuantumCircuit, got {type(qc).__name__}.")
# Check that the input circuit contains no measurements
if any(instr.op.name == "measure" for instr in qc.data):
raise ValueError(
"The input circuit contains measurement instructions, "
"which break unitarity. Remove measurements before "
"calling compare_unitary."
)
# Apply the pass to a copy to avoid mutating the original
transformed_qc = self(qc.copy())
# Check that the transformed circuit contains no measurements
if any(instr.op.name == "measure" for instr in transformed_qc.data):
raise ValueError(
"The transformed circuit contains measurement "
"instructions, which break unitarity. The pass should "
"not introduce measurements."
)
return qc.compare_unitary(transformed_qc, precision, ignore_gphase)
[docs]
def visualize(self, qc: QuantumCircuit) -> None:
"""Print a before/after visualisation of this pass applied to *qc*.
The method copies *qc*, applies the pass, and prints both the
original and the transformed circuit to the console.
Parameters
----------
qc : QuantumCircuit
The input quantum circuit to visualise.
Examples
--------
>>> from qrisp import QuantumCircuit, CircuitPass
>>> from qrisp.circuit.pass_management.passes.fuse_adjacents import fuse_adjacents
>>> qc = QuantumCircuit(2)
>>> qc.cx(0, 1)
>>> qc.cx(0, 1)
>>> fuse_adjacents.visualize(qc)
"""
# Apply the pass to a copy to avoid mutating the original
transformed_qc = self(qc.copy())
pass_name = self.__name__
width = 60
# ── Banner ────────────────────────────────────────────────────
print(f" {pass_name} ".center(width, "="))
# ── Before ────────────────────────────────────────────────────
print(" Before ".center(width, "─"))
print(qc)
# ── After ─────────────────────────────────────────────────────
print(" After ".center(width, "─"))
print(transformed_qc)
# ── Footer ────────────────────────────────────────────────────
print("=" * width)
[docs]
def compare_measurement(
self,
qc: QuantumCircuit,
precision: int = 6,
backend: Any = None,
) -> bool:
"""Verify that this :class:`CircuitPass` leaves measurement statistics
invariant when applied to the given :class:`~qrisp.QuantumCircuit`.
The method copies *qc*, applies the pass to the copy, and then
compares the measurement distributions of the original and the
transformed circuit using :meth:`QuantumCircuit.run`.
By default, the method runs in analytic mode (no shot noise): the
default backend's ``shots`` option is ``None``, yielding exact
probability distributions of type ``dict[bitstring, float]``.
Parameters
----------
qc : QuantumCircuit
The input quantum circuit to test the pass against.
precision : int, optional
The number of decimal places of agreement required between
corresponding probabilities. A pair of outcomes contributes a
mismatch when ``abs(p_original - p_transformed) >= 10 ** -precision``.
The default is 6.
backend : Backend or None, optional
The backend used for simulation. If ``None``, the Qrisp default
backend is used (which runs in analytic mode with ``shots=None``).
Returns
-------
bool
``True`` if the pass preserves the measurement distribution up
to the given precision, ``False`` otherwise.
Raises
------
TypeError
If *qc* is not a :class:`~qrisp.QuantumCircuit`.
"""
if not isinstance(qc, QuantumCircuit):
raise TypeError(f"Expected a QuantumCircuit, got {type(qc).__name__}.")
if backend is None:
from qrisp.default_backend import def_backend
backend = def_backend
# Apply the pass to a copy to avoid mutating the original
transformed_qc = self(qc.copy())
# Obtain measurement statistics. When shots is None (the default
# for DefaultBackend) the results are exact probability
# distributions (dict[str, float]).
original_counts = qc.run(shots=None, backend=backend)
transformed_counts = transformed_qc.run(shots=None, backend=backend)
# Gather all observed outcome keys
all_keys = set(original_counts.keys()) | set(transformed_counts.keys())
tolerance = 10.0**-precision
for key in all_keys:
p_orig = original_counts.get(key, 0.0)
p_trans = transformed_counts.get(key, 0.0)
# Handle the empty-circuit / no-measurement case where
# the simulator may return {"": None} when shots=None.
if p_orig is None:
p_orig = 0.0
if p_trans is None:
p_trans = 0.0
if abs(p_orig - p_trans) >= tolerance:
return False
return True