Source code for qbraid.interface.circuit_equality

# Copyright (C) 2024 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.

"""
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)