# annotations import won't be needed after minimum version becomes 3.14 (PEP 749)
from __future__ import annotations
import inspect
from abc import ABC, ABCMeta, abstractmethod
from typing import Type
import numpy as np
from qutip import Qobj
from qutip_qip.operations.namespace import NameSpace
_read_only_set: set[str] = set(
(
"namespace",
"num_qubits",
"num_ctrl_qubits",
"num_params",
"is_parametric",
"is_controlled",
"ctrl_value",
"self_inverse",
"is_clifford",
"target_gate",
"latex_str",
)
)
class _GateMetaClass(ABCMeta):
def __init__(cls, name, bases, attrs):
"""
This method is automatically invoked during class creation. It validates that
the new gate class has a unique name within its specific namespace (defaulting
to "std"). If the same gate already exists in that namespace, it raises a strict
TypeError to prevent ambiguous gate definitions.
This is required since in the codebase at several places like decomposition we
check for e.g. gate.name == 'X', which is corrupted if user defines a gate with
the same name.
"""
super().__init__(name, bases, attrs)
# Don't register the Abstract Gate Classes or private helpers
if inspect.isabstract(cls):
cls._is_frozen = True
return
# _is_frozen class attribute (flag) signals class (or subclass) is built,
# don't overwrite any defaults like num_qubits etc in __setattr__.
cls._is_frozen = True
# Namespace being None corresponds to Temporary Gates
# Only if Namespace is not None register the gate
if (namespace := getattr(cls, "namespace", None)) is not None:
# We are checking beforehand because in case of Controlled Gate
# two key's refer to the same controlled gate:
# gate_name, (target_gate.name, num_ctrl_qubits, ctrl_value)
# If suppose (target_gate=X, num_ctrl_qubits=1, ctrl_value=0) existed
# but we were redefining it with a different name, the cls.name insert
# step would go through, but wrt. second key won't and will throw an error.
# This will lead to leakage in the namespace i.e. classes which don't exist but are in the namespace.
existing_gate = namespace.get(cls.name)
if existing_gate is not None:
try:
# Check if both classes originate from the exact same physical file.
# If they do, this is a namespace alias/reload
# This is needed because qutip.qip import support is still needed
if inspect.getfile(existing_gate) == inspect.getfile(cls):
return
else:
raise ValueError(
f"Existing {cls.name} in namespace {namespace}"
)
except TypeError:
pass # Fallback
# The basic principle is don't define a gate class if it already exists
if cls.is_controlled:
cls.namespace.register(
(
cls.target_gate.name,
cls.num_ctrl_qubits,
cls.ctrl_value,
),
cls,
)
cls.namespace.register(cls.name, cls)
def __setattr__(cls, name: str, value: any) -> None:
"""
One of the main purpose of this meta class is to enforce read-only constraints
on specific class attributes. This prevents critical attributes from being
overwritten after definition, while still allowing them to be set during inheritance.
For example:
class X(Gate):
num_qubits = 1 # Allowed (during class creation)
But:
X.num_qubits = 2 # Raises AttributeError (prevention of overwrite)
This is required since num_qubits etc. are class attributes (shared by all object instances).
"""
# cls.__dict__.get() instead of getattr() ensures we don't
# accidentally inherit the True flag from a parent class for _is_frozen.
if cls.__dict__.get("_is_frozen", False) and name in _read_only_set:
raise AttributeError(f"{name} is read-only!")
super().__setattr__(name, value)
def __str__(cls) -> str:
# Don't remove these getattr statement otherwise doctest will fail
# The reason is because how Sphinx builds and runs doctest even on abstract classes.
gatename = getattr(cls, "name", cls.__name__)
return f"Gate({gatename})"
def __repr__(cls) -> str:
gatename = getattr(cls, "name", cls.__name__)
num_qubits = getattr(cls, "num_qubits", None)
return f"Gate({gatename}, num_qubits={num_qubits})"
[docs]
class Gate(ABC, metaclass=_GateMetaClass):
r"""
Abstract base class for a quantum gate.
Concrete gate classes or gate implementations should be defined as subclasses
of this class.
Attributes
----------
name : str
The name of the gate. If not manually set, this defaults to the
class name. This is a class attribute; modifying it affects all
instances.
num_qubits : int
The number of qubits the gate acts upon. This is a mandatory
class attribute for subclasses.
self_inverse: bool
Indicates if the gate is its own inverse (e.g., $U = U^{-1}$).
Default value is False.
is_clifford: bool
Indicates if the gate belongs to the Clifford group, which maps
Pauli operators to Pauli operators. Default value is False
latex_str : str
The LaTeX string representation of the gate (used for circuit drawing).
Defaults to the class name if not provided.
"""
# __slots__ in Python are meant to fixed-size array of attribute values
# instead of a default dynamic sized __dict__ created in object instances.
# This helps save memory, faster lookup time & restrict adding new attributes to class.
__slots__ = ()
namespace: NameSpace | None = None
name: str
num_qubits: int
self_inverse: bool = False
is_clifford: bool = False
latex_str: str
is_parametric: bool = False
is_controlled: bool = False
def __init_subclass__(cls, **kwargs) -> None:
"""
Automatically runs when a new subclass is defined via inheritance.
This method sets the ``name`` and ``latex_str`` attributes if
they are not defined in the subclass. It also validates that ``num_qubits``
is a non-negative integer, ``is_clifford``, ``self_inverse`` are
bool and ``inverse`` method is not defined if ``self_inverse`` is set True.
"""
# Skip the below check for an abstract class
if inspect.isabstract(cls):
return super().__init_subclass__(**kwargs)
# If name attribute in subclass is not defined, set it to the name of the subclass
# e.g. class H(Gate):
# pass
# print(H.name) -> 'H'
# e.g. class H(Gate):
# name = "Hadamard"
# pass
# print(H.name) -> 'Hadamard'
if "name" not in vars(cls):
cls.name = cls.__name__
# Same as above for attribute latex_str (used in circuit draw)
if "latex_str" not in vars(cls):
cls.latex_str = cls.__name__
# Assert num_qubits is a non-negative integer
num_qubits = getattr(cls, "num_qubits", None)
if (type(num_qubits) is not int) or (num_qubits < 0):
raise TypeError(
f"Class '{cls.name}' attribute 'num_qubits' must be a non-negative integer, "
f"got {type(num_qubits)} with value {num_qubits}."
)
# get_qobj method must take the parameter - dtype
get_qobj_func = getattr(cls, "get_qobj")
if not callable(get_qobj_func):
raise TypeError(
f"Attribute 'get_qobj' in '{cls.name}' must be a callable method."
)
if "dtype" not in inspect.signature(get_qobj_func).parameters:
raise SyntaxError(
f"Class '{cls.name}' method 'get_qobj()' must always take the parameter dtype "
f" but got '{inspect.signature(get_qobj_func).parameters}'."
)
# Check is_clifford is a bool
if type(cls.is_clifford) is not bool:
raise TypeError(
f"Class '{cls.name}' attribute 'is_clifford' must be a bool, "
f"got {type(cls.is_clifford)} with value {cls.is_clifford}."
)
# Check self_inverse is a bool
if type(cls.self_inverse) is not bool:
raise TypeError(
f"Class '{cls.name}' attribute 'self_inverse' must be a bool, "
f"got {type(cls.self_inverse)} with value {cls.self_inverse}."
)
# Check is_parametric is a bool
if type(cls.is_parametric) is not bool:
raise TypeError(
f"Class '{cls.name}' attribute 'is_parametric' must be a bool, "
f"got {type(cls.is_parametric)} with value {cls.is_parametric}."
)
# Check is_controlled is a bool
if type(cls.is_controlled) is not bool:
raise TypeError(
f"Class '{cls.name}' attribute 'is_controlled' must be a bool, "
f"got {type(cls.is_controlled)} with value {cls.is_controlled}."
)
# Can't define inverse() method if self_inverse is set True
if cls.self_inverse and "inverse" in cls.__dict__:
raise TypeError(
f"Gate '{cls.name}' is marked as self_inverse=True. "
f"You are not allowed to override the 'inverse()' method. "
f"Remove the method; the base class handles it automatically."
)
return super().__init_subclass__(**kwargs)
def __init__(self) -> None:
"""
This method is overwritten in case of Parametrized and Controlled Gates.
"""
raise TypeError(
f"Gate '{self.name}' can't be initialised. "
f"If your gate requires parameters, it must inherit from 'ParametricGate'. "
f"Or if it must be controlled, it must inherit from 'ControlledGate'."
)
[docs]
@staticmethod
@abstractmethod
def get_qobj(dtype: str = "dense") -> Qobj:
"""
Get the :class:`qutip.Qobj` representation of the gate operator.
Returns
-------
qobj : :obj:`qutip.Qobj`
The compact gate operator as a unitary matrix.
"""
raise NotImplementedError
[docs]
@classmethod
def inverse(cls) -> Type[Gate]:
"""
Return the inverse of the gate.
If ``self_inverse`` is True, returns ``self``. Otherwise,
returns the specific inverse gate class.
Returns
-------
Type[Gate]
A Gate instance representing $G^{-1}$.
"""
if cls.self_inverse:
return cls
return get_unitary_gate(f"{cls.name}_inv", cls.get_qobj().dag())
[docs]
def get_unitary_gate(
gate_name: str, U: Qobj, gate_namespace: NameSpace | None = None
) -> Type[Gate]:
"""
Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U.
"""
# Check whether U is unitary
n = np.log2(U.shape[0])
if n != np.log2(U.shape[1]):
raise ValueError("The U must be square matrix.")
if n % 1 != 0:
raise ValueError("The unitary U must have dim NxN, where N=2^n")
if not np.allclose((U * U.dag()).full(), np.eye(U.shape[0])):
raise ValueError("U must be a unitary matrix")
class _CustomGate(Gate):
__slots__ = ()
namespace = gate_namespace
name = gate_name
num_qubits = int(n)
self_inverse = U == U.dag()
@staticmethod
def get_qobj(dtype=U.dtype):
return U.to(dtype)
return _CustomGate