Source code for qbraid.interface.circuit_equality

# Copyright 2025 qBraid
#
# 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.

"""
Module for calculating unitary of quantum circuit/program

"""
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from qbraid.programs import load_program

if TYPE_CHECKING:
    import qbraid


def match_global_phase(a: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """
    Matches the global phase of two numpy arrays.

    This function aligns the global phases of two matrices by applying a phase shift based
    on the position of the largest entry in one matrix. It computes and adjusts for the
    phase difference at this position, resulting in two phase-aligned matrices. The output,
    (a', b'), indicates that a' == b' is equivalent to a == b * exp(i * t) for some phase t.

    Args:
        a (np.ndarray): The first input matrix.
        b (np.ndarray): The second input matrix.

    Returns:
        tuple[np.ndarray, np.ndarray]: A tuple of the two matrices `(a', b')`, adjusted for
                                       global phase. If shapes of `a` and `b` do not match or
                                       either is empty, returns copies of the original matrices.
    """
    if a.shape != b.shape or a.size == 0:
        return np.copy(a), np.copy(b)

    k = max(np.ndindex(*a.shape), key=lambda t: abs(b[t]))

    def dephase(v):
        r = np.real(v)
        i = np.imag(v)

        if i == 0:
            return -1 if r < 0 else 1
        if r == 0:
            return 1j if i < 0 else -1j

        return np.exp(-1j * np.arctan2(i, r))

    return a * dephase(a[k]), b * dephase(b[k])


[docs] def assert_allclose_up_to_global_phase(a: np.ndarray, b: np.ndarray, atol: float, **kwargs) -> None: """ Checks if two numpy arrays are equal up to a global phase, within a specified tolerance, i.e. if a ~= b * exp(i t) for some t. Args: a (np.ndarray): The first input array. b (np.ndarray): The second input array. atol (float): The absolute error tolerance. **kwargs: Additional keyword arguments to pass to `np.testing.assert_allclose`. Raises: AssertionError: The matrices aren't nearly equal up to global phase. """ a, b = match_global_phase(a, b) np.testing.assert_allclose(actual=a, desired=b, atol=atol, **kwargs)
[docs] def circuits_allclose( # pylint: disable=too-many-arguments circuit0: qbraid.programs.QPROGRAM, circuit1: qbraid.programs.QPROGRAM, index_contig: bool = False, allow_rev_qubits: bool = False, strict_gphase: bool = False, atol: float = 1e-7, ) -> bool: """Check if quantum program unitaries are equivalent. Args: circuit0 (:data:`~qbraid.programs.QPROGRAM`): First quantum program to compare circuit1 (:data:`~qbraid.programs.QPROGRAM`): Second quantum program to compare index_contig: If True, calculates circuit unitaries using contiguous qubit indexing. allow_rev_qubits: Whether to count identical circuits with reversed qubit ordering as equivalent. strict_gphase: If False, disregards global phase when verifying equivalence of the input circuit's unitaries. atol: Absolute tolerance parameter for np.allclose function. Returns: True if the input circuits pass unitary equality check """ def unitary_equivalence_check(unitary0, unitary1, unitary_rev=None): if strict_gphase: return np.allclose(unitary0, unitary1) or ( allow_rev_qubits and np.allclose(unitary0, unitary_rev) ) try: assert_allclose_up_to_global_phase(unitary0, unitary1, atol=atol) except AssertionError: if allow_rev_qubits: try: assert_allclose_up_to_global_phase(unitary0, unitary_rev, atol=atol) except AssertionError: return False else: return False return True program0 = load_program(circuit0) program1 = load_program(circuit1) if index_contig: program0.remove_idle_qubits() program1.remove_idle_qubits() unitary0 = program0.unitary() unitary1 = program1.unitary() unitary_rev = program1.unitary_rev_qubits() return unitary_equivalence_check(unitary0, unitary1, unitary_rev)