Source code for qilisdk.digital.circuit_transpiler_passes.fuse_single_qubit_gates_pass

# Copyright 2023 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import math

import numpy as np

from qilisdk.digital import Circuit
from qilisdk.digital.circuit_transpiler_passes.decompose_to_canonical_basis_pass import SingleQubitGateBasis
from qilisdk.digital.exceptions import GateHasNoMatrixError
from qilisdk.digital.gates import (
    RX,
    RY,
    RZ,
    U3,
    Gate,
)

from .circuit_transpiler_pass import CircuitTranspilerPass
from .numeric_helpers import (
    _is_close_mod_2pi,
    _wrap_angle,
    _zyz_from_unitary,
)


[docs] class FuseSingleQubitGatesPass(CircuitTranspilerPass): """ Fuse maximal adjacent runs of 1-qubit unitary gates per wire into a basis-respecting sequence: - For ``SingleQubitGateBasis.U3``: emit a single ``U3``. - For ``SingleQubitGateBasis.RxRyRz``: emit ``RZ``, ``RY``, ``RX`` when recognizable, otherwise emit the equivalent ``RZ(phi) · RY(theta) · RZ(gamma)`` sequence. Any gate that is not a 1-qubit unitary matrix (including measurements or unsupported/no-matrix gates) acts as a boundary for the touched qubits. Always returns a NEW circuit. """ def __init__(self, single_qubit_basis: SingleQubitGateBasis = SingleQubitGateBasis.U3) -> None: if not isinstance(single_qubit_basis, SingleQubitGateBasis): raise TypeError( f"single_qubit_basis must be a SingleQubitGateBasis value (got {type(single_qubit_basis).__name__})." ) self._single_qubit_basis = single_qubit_basis @property
[docs] def single_qubit_basis(self) -> SingleQubitGateBasis: return self._single_qubit_basis
@staticmethod def _try_single_qubit_unitary(gate: Gate) -> np.ndarray | None: """Return a 2x2 unitary matrix for a single-qubit gate when available. Args: gate (Gate): Gate candidate to validate and convert to matrix form. Returns: np.ndarray | None: Complex 2x2 unitary matrix when ``gate`` is a valid one-qubit unitary; otherwise ``None``. """ if gate.nqubits != 1: return None try: unitary = np.array(gate.matrix, dtype=complex, copy=True) except GateHasNoMatrixError: return None if unitary.shape != (2, 2): return None identity = np.eye(2, dtype=complex) if not np.allclose(unitary.conj().T @ unitary, identity, atol=1e-10): return None return unitary
[docs] def run(self, circuit: Circuit) -> Circuit: """Fuse adjacent one-qubit unitary sequences into canonical single gates. Non-unitary or multi-qubit gates define boundaries and force flushing of pending fused unitaries on touched qubits. Args: circuit (Circuit): Input circuit to optimize. Returns: Circuit: New circuit with fused one-qubit runs and unchanged behavior. """ output_gates: list[Gate] = [] pending: dict[int, np.ndarray] = {} def flush_pending_qubit(qubit: int) -> None: """Emit the fused gate accumulated for a qubit, if any. This helper mutates ``pending`` and appends a basis-respecting canonical sequence to ``output_gates``. Args: qubit (int): Qubit whose pending fused unitary is materialized in the configured single-qubit basis. """ if qubit not in pending: return U = pending.pop(qubit) output_gates.extend(self._emit_fused_basis_gates(qubit, U)) for gate in circuit.gates: unitary = self._try_single_qubit_unitary(gate) if unitary is not None: qubit = gate.qubits[0] pending[qubit] = unitary @ pending[qubit] if qubit in pending else unitary continue # Boundary: flush pending runs that touch gate qubits. for qubit in gate.qubits: flush_pending_qubit(qubit) output_gates.append(gate) # flush any remaining while pending: flush_pending_qubit(next(iter(pending))) output_circuit = Circuit(circuit.nqubits) for gate in output_gates: output_circuit.add(gate) self.append_circuit_to_context(output_circuit) return output_circuit
def _emit_fused_basis_gates(self, qubit: int, unitary: np.ndarray) -> list[Gate]: theta, phi, gamma = _zyz_from_unitary(unitary) theta, phi, gamma = float(theta), float(phi), float(gamma) theta = _wrap_angle(theta) phi = _wrap_angle(phi) gamma = _wrap_angle(gamma) if self._single_qubit_basis == SingleQubitGateBasis.U3: return [U3(qubit, theta=theta, phi=phi, gamma=gamma)] if self._single_qubit_basis == SingleQubitGateBasis.RxRyRz: if _is_close_mod_2pi(theta, 0.0): # pure Z: RZ(ph + lam) return [RZ(qubit, phi=_wrap_angle(phi + gamma))] if _is_close_mod_2pi(phi, 0.0) and _is_close_mod_2pi(gamma, 0.0): return [RY(qubit, theta=theta)] if _is_close_mod_2pi(phi, math.pi) and _is_close_mod_2pi(gamma, math.pi): return [RY(qubit, theta=-theta)] if _is_close_mod_2pi(phi, -math.pi / 2.0) and _is_close_mod_2pi(gamma, math.pi / 2.0): return [RX(qubit, theta=theta)] if _is_close_mod_2pi(phi, math.pi / 2.0) and _is_close_mod_2pi(gamma, -math.pi / 2.0): return [RX(qubit, theta=-theta)] return [ RZ(qubit, phi=gamma), RY(qubit, theta=theta), RZ(qubit, phi=phi), ] raise TypeError(f"Unsupported single-qubit basis {self._single_qubit_basis}")