Source code for qrisp.circuit.pass_management.passes.combine_single_qubit_gates
"""********************************************************************************
* 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 numpy as np
from qrisp.circuit.operation import ControlledOperation, U3Gate
from qrisp.circuit.pass_management.circuit_pass import CircuitPass
from qrisp.circuit.quantum_circuit import QuantumCircuit
def _apply_combined_gates(qc_new: QuantumCircuit, gate_list: list, qb) -> None:
"""Flush a sequence of single-qubit gates on *qb* into *qc_new*.
The unitaries of all gates in *gate_list* are multiplied together
(last gate first, since it acts on the state first). If the product
equals the identity (within numerical tolerance), the gates cancel
entirely and nothing is appended. Otherwise a single operation is
appended: the original gate when the sequence length is 1, or a
generic unitary for longer sequences.
Parameters
----------
qc_new : QuantumCircuit
The output circuit being built.
gate_list : list
Stack of single-qubit gate operations (last appended = last to act).
qb : Qubit
The qubit the gates act on.
"""
if not gate_list:
return
n = len(gate_list)
# Multiply unitaries in reverse order: the gate that was applied
# *last* is the rightmost factor in the product.
m = np.eye(2, dtype=complex)
while gate_list:
gate = gate_list.pop()
m = m @ gate.get_unitary()
# If the combined unitary is the identity (up to numerical precision),
# the gates cancel completely.
if np.linalg.norm(m - np.eye(2)) < 1e-10:
return
# For a single gate there is nothing to combine — just re-emit it.
if n == 1:
qc_new.append(gate, [qb])
return
# Multiple gates: emit a single unitary encapsulating the product.
qc_new.unitary(m, [qb])
[docs]
@CircuitPass
def combine_single_qubit_gates(qc: QuantumCircuit) -> QuantumCircuit:
"""Combine adjacent single-qubit gates into unitary operations.
This pass scans the circuit instruction-by-instruction and accumulates
consecutive single-qubit gates on each qubit. When an interruption is
encountered (multi-qubit gate, allocation, measurement, …) the
pending gates are flushed: their unitaries are multiplied together and
emitted as a single operation, cancelling any sequences that reduce to
the identity.
**Recursive processing**
Composite gates (gates with a ``definition``) are processed
recursively so that single-qubit gate sequences inside compound
operations are also combined.
Controlled operations whose ``base_operation`` carries a definition
are handled analogously: the base definition is combined in place
and the controlled wrapper is preserved.
This pass is designed as a local optimisation that reduces gate count
and depth without changing the overall circuit semantics. It is
especially useful after transpilation passes that may introduce
redundant single-qubit rotations.
Parameters
----------
qc : QuantumCircuit
The input circuit.
Returns
-------
QuantumCircuit
A new circuit with adjacent single-qubit gates combined.
Examples
--------
>>> from qrisp import PassManager, combine_single_qubit_gates
>>> from qrisp import QuantumCircuit
>>> qc = QuantumCircuit(1)
>>> qc.x(0)
>>> qc.z(0)
>>> qc.h(0) # X, Z, Y on the same qubit
>>> pm = PassManager()
>>> pm += combine_single_qubit_gates
>>> optimized_qc = pm.run(qc)
>>> # X, Z, Y combined into a single U3 gate
>>> print(optimized_qc)
┌──────────────┐
qb_64: ┤ U3(π/2,3π,0) ├
└──────────────┘
"""
# Per-qubit stacks of single-qubit gates waiting to be flushed.
qb_dic = {qb: [] for qb in qc.qubits}
qc_new = qc.clearcopy()
for instr in qc.data:
op = instr.op
# Determine whether this instruction is a "barrier" that forces
# us to flush the accumulated single-qubit gates first.
is_single_qubit_gate = isinstance(op, U3Gate)
if not is_single_qubit_gate or op.abstract_params:
# Flush pending gates for every qubit touched by this instruction.
for qb in instr.qubits:
_apply_combined_gates(qc_new, qb_dic[qb], qb)
# ---- Recursive processing of composite gates ----
if isinstance(op, ControlledOperation):
# If the controlled operation's base gate itself has a
# definition, recursively combine single-qubit gates inside
# that definition, then re-wrap in a ControlledOperation.
if op.base_operation.definition:
instr = instr.copy()
optimized_base = combine_single_qubit_gates(op.base_operation.definition).to_gate(
name=op.base_operation.name
)
instr.op = ControlledOperation(
optimized_base,
num_ctrl_qubits=len(op.ctrl_state),
ctrl_state=op.ctrl_state,
)
elif op.definition:
# Generic composite gate: recursively optimise its definition.
instr = instr.copy()
instr.op = instr.op.copy()
instr.op.definition = combine_single_qubit_gates(op.definition)
qc_new.append(instr)
else:
# Single-qubit gate — defer; will be combined with neighbours.
qb_dic[instr.qubits[0]].append(op)
# Flush any remaining gates at the end of the circuit.
for qb in qc.qubits:
_apply_combined_gates(qc_new, qb_dic[qb], qb)
return qc_new