Source code for mimiqcircuits.operations.gates.customdiagonal

#
# Copyright © 2023-2026 QPerfect. All Rights Reserved.
#
# 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.
#
"""Custom diagonal gate definition."""

import numpy as np
import symengine as se
import sympy as sp

import mimiqcircuits as mc
from mimiqcircuits.operations.gates.gate import Gate
from mimiqcircuits.operations.decompositions.matrix_decompositions.utils import (
    _as_numpy_numeric,
    _to_symengine_matrix,
)


def _fwht(a):
    """Fast Walsh-Hadamard transform, unnormalized."""
    a = np.array(a, dtype=float)
    h = 1
    while h < len(a):
        for i in range(0, len(a), 2 * h):
            x = a[i : i + h].copy()
            y = a[i + h : i + 2 * h].copy()
            a[i : i + h] = x + y
            a[i + h : i + 2 * h] = x - y
        h *= 2
    return a


[docs] class GateCustomDiagonal(Gate): """Gate diagonal in the computational basis, given by its diagonal. Same role as :class:`GateCustom`, but only the ``2**n`` diagonal entries are stored, so a wide diagonal block costs ``2**n`` numbers instead of ``4**n``. Every entry must be a phase, ``abs(d) == 1``. The entries follow the diagonal of the dense matrix, in the basis ``|0...0>, ..., |1...1>`` with the first target qubit as the most significant bit — the same convention as :class:`GateCustom`. Examples: >>> from mimiqcircuits import Circuit, GateCustomDiagonal >>> c = Circuit() >>> c.push(GateCustomDiagonal([1, 1, 1, -1]), 0, 1) 2-qubit circuit with 1 instruction: └── CustomDiagonal(...) @ q[0:1] <BLANKLINE> """ _name = "CustomDiagonal" _num_qubits = None _qregsizes = None
[docs] def __init__(self, diagonal): super().__init__() if isinstance(diagonal, se.Matrix): entries = list(diagonal) elif isinstance(diagonal, (np.ndarray, list, tuple)): entries = list(np.asarray(diagonal, dtype=object).ravel()) else: raise TypeError( f"{type(diagonal)} not supported in GateCustomDiagonal, " "use a sequence, numpy.ndarray or symengine.Matrix." ) num_qubits = len(entries).bit_length() - 1 if num_qubits < 1 or len(entries) != 2**num_qubits: raise ValueError("Wrong number of entries for the diagonal") d = se.Matrix(len(entries), 1, entries) try: numeric = _as_numpy_numeric(d) except TypeError: numeric = None if numeric is not None: if not np.allclose(np.abs(numeric), 1.0, rtol=0, atol=1e-8): raise ValueError("Diagonal is not unitary") # Store numeric entries as floats, so that a diagonal given as # integers compares equal to the same one given as floats. d = _to_symengine_matrix(numeric.reshape(-1, 1)) self._d = d self._num_qubits = num_qubits self._qregsizes = [ num_qubits, ]
def _matrix(self): return se.diag(*list(self._d))
[docs] def matrix(self): """Dense matrix, entries evaluated numerically where possible. Not inherited from :class:`AbstractOperator` for the same reason as :meth:`GateCustom.matrix`: that version memoises on the *class* for parameter-free operators, which would make every instance share whichever matrix was built first. """ return se.diag(*self.diagonal())
[docs] def diagonal(self): """The stored diagonal entries, evaluated numerically where possible.""" out = [] for x in self._d: try: out.append(complex(x)) except Exception: out.append(x) return out
[docs] def unwrappeddiagonal(self): """The diagonal as a ``numpy.ndarray[complex128]``. Raises: TypeError: if any entry is still symbolic. """ return _as_numpy_numeric(self._d).ravel()
[docs] def unwrappedmatrix(self): return np.diag(self.unwrappeddiagonal())
@property def num_qubits(self): return self._num_qubits
[docs] def inverse(self): return GateCustomDiagonal([se.conjugate(x) for x in self._d])
def __repr__(self): return self.pretty_print() def __str__(self): return f"{self._name}(...)"
[docs] def pretty_print(self): result = f"{self._num_qubits}-qubit GateCustomDiagonal:\n" entries = list(self._d) for i, entry in enumerate(entries): result += "├── " if i < len(entries) - 1 else "└── " result += str(entry) if i < len(entries) - 1: result += "\n" return result
[docs] def evaluate(self, d): entries = [ ( entry if isinstance(entry, (float, int)) else sp.sympify(entry).subs(d) ) for entry in self._d ] return GateCustomDiagonal([se.sympify(entry) for entry in entries])
def _decompose(self, circ, qubits, bits, zvars): """Expand the phases in the Walsh basis. With ``phi(x) = sum_S a_S (-1)^{|x & S|}``, the gate is ``e^{i a_0} prod_{S != 0} exp(i a_S Z_S)``, and each parity term is one ``GateRNZ`` on the qubits of ``S`` (a ``GateRZ`` when ``S`` is a single qubit). Not routed through :class:`GateCustom`: that would build a ``4**n``-entry matrix for a gate that needs ``2**n``, and QSD would then spend a multiple of that many gates rediscovering a structure this expansion writes down directly. """ n = self.num_qubits if len(qubits) != n: raise ValueError( f"GateCustomDiagonal expects {n} qubits, got {len(qubits)}" ) alpha = _fwht(np.angle(self.unwrappeddiagonal())) / (1 << n) for s in range(1, 1 << n): if abs(alpha[s]) < 1e-14: continue # bit `n - 1 - p` of the mask selects the p-th target, matching the # most-significant-first order of the diagonal itself qs = [qubits[p] for p in range(n) if (s >> (n - 1 - p)) & 1] if len(qs) == 1: circ.push(mc.GateRZ(-2 * alpha[s]), qs[0]) else: circ.push(mc.GateRNZ(len(qs), -2 * alpha[s]), *qs) # alpha[0] is the global phase left over if abs(alpha[0]) >= 1e-14: circ.push(mc.GateU(0, 0, 0, alpha[0]), qubits[0]) return circ
__all__ = ["GateCustomDiagonal"]