Source code for qutip_qip.operations.controlled

import inspect
import warnings
from abc import abstractmethod
from functools import partial
from typing import Final, Type

from qutip import Qobj
from qutip_qip.operations import (
    Gate,
    NameSpace,
    controlled_gate_unitary,
)


class class_or_instance_method:
    """
    Binds a method to the instance if called on an instance,
    or to the class if called on the class.
    """

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # Called on the class (e.g., CX.get_qobj())
        if instance is None:
            return partial(self.func, owner)

        # Called on the instance (e.g., CRX(0.5).get_qobj())
        return partial(self.func, instance)


[docs] class ControlledGate(Gate): r""" Abstract base class for controlled quantum gates. A controlled gate applies a target unitary operation only when the control qubits are in a specific state. Attributes ---------- target_gate : :class:`.Gate` The gate to be applied to the target qubits. num_ctrl_qubits : int The number of qubits acting as controls. ctrl_value : int The decimal value of the control state required to execute the unitary operator on the target qubits. Example: If the gate should execute when the 0-th qubit is $|1\rangle$ set ``ctrl_value=1``. If the gate should execute when two control qubits are $|10\rangle$ (binary 10), set ``ctrl_value=0b10``. """ __slots__ = ("_target_inst",) is_controlled: Final[bool] = True num_ctrl_qubits: int ctrl_value: int target_gate: Type[Gate] def __init_subclass__(cls, **kwargs) -> None: """ Validates the subclass definition. """ super().__init_subclass__(**kwargs) if inspect.isabstract(cls): return # Must have a target_gate target_gate = getattr(cls, "target_gate", None) if target_gate is None or not issubclass(target_gate, Gate): raise TypeError( f"Class '{cls.__name__}' attribute 'target_gate' must be a Gate class, " f"got {type(target_gate)} with value {target_gate}." ) # Check num_ctrl_qubits is a positive integer num_ctrl_qubits = getattr(cls, "num_ctrl_qubits", None) if (type(num_ctrl_qubits) is not int) or (num_ctrl_qubits < 1): raise TypeError( f"Class '{cls.__name__}' attribute 'num_ctrl_qubits' must be a positive integer, " f"got {type(num_ctrl_qubits)} with value {num_ctrl_qubits}." ) # Check num_ctrl_qubits < num_qubits if not cls.num_ctrl_qubits < cls.num_qubits: raise ValueError( f"{cls.__name__}: 'num_ctrl_qubits' must be less than the 'num_qubits'" ) # Check num_ctrl_qubits + target_gate.num_qubits = num_qubits if cls.num_ctrl_qubits + cls.target_gate.num_qubits != cls.num_qubits: raise AttributeError( f"'num_ctrls_qubits' {cls.num_ctrl_qubits} + 'target_gate qubits' {cls.target_gate.num_qubits} must be equal to 'num_qubits' {cls.num_qubits}" ) cls._validate_control_value() # If the target gate is parametric e.g. RX, so is the overall controlled gate e.g. CRX cls.is_parametric = cls.target_gate.is_parametric # Default self_inverse # Don't replace cls.__dict__ with hasattr() that does a MRO search if "self_inverse" not in cls.__dict__: cls.self_inverse = cls.target_gate.self_inverse # In the circuit plot, only the target gate is shown. # The control has its own symbol. if "latex_str" not in cls.__dict__: cls.latex_str = cls.target_gate.latex_str if not cls.is_controlled: raise ValueError( f"Class '{cls.name}' method 'is_controlled' must be set to True." ) def __init__(self, *args, **kwargs) -> None: self._target_inst = self.target_gate(*args, **kwargs) def __getattr__(self, name: str) -> any: """ If an attribute (like 'arg_value') or method (like 'validate_params') isn't found on the ControlledGate, Python falls back to this method. We forward the request to the underlying target gate instance. """ return getattr(self._target_inst, name) def __setattr__(self, name, value) -> None: """ Intercept attribute assignment. If it's our internal storage variable, set it normally on this instance. Otherwise, forward the assignment to the underlying target gate. """ if name == "_target_inst": super().__setattr__(name, value) else: setattr(self._target_inst, name, value) # Although target_gate is specified as a class attribute, It has been # been made an abstract method to make ControlledGate abstract (required in Metaclass) # This is because Python currently doesn't support abstract class attributes. @property @abstractmethod def target_gate() -> Type[Gate]: pass @classmethod def _validate_control_value(cls) -> None: """ Internal validation for the control value. Raises ------ TypeError If ctrl_value is not an integer. ValueError If ctrl_value is negative or exceeds the maximum value possible for the number of control qubits ($2^N - 1$). """ if type(cls.ctrl_value) is not int: raise TypeError(f"Control value must be an int, got {cls.ctrl_value}") if cls.ctrl_value < 0 or cls.ctrl_value > (1 << cls.num_ctrl_qubits) - 1: raise ValueError( f"Control value can't be negative and can't be greater than " f"2^num_ctrl_qubits - 1, got {cls.ctrl_value}" ) @class_or_instance_method def get_qobj(cls_or_self, dtype: str = "dense") -> Qobj: """ Construct the full Qobj representation of the controlled gate. Returns ------- qobj : qutip.Qobj The unitary matrix representing the controlled operation. """ if isinstance(cls_or_self, type): target_gate = cls_or_self.target_gate else: target_gate = cls_or_self._target_inst return controlled_gate_unitary( U=target_gate.get_qobj(dtype), num_controls=cls_or_self.num_ctrl_qubits, control_value=cls_or_self.ctrl_value, ) @class_or_instance_method def inverse(cls_or_self) -> Gate | Type[Gate]: if cls_or_self.self_inverse: inverse_gate = cls_or_self # Non-parametrized Gates e.g. S elif isinstance(cls_or_self, type): inverse_gate = get_controlled_gate( cls_or_self.target_gate.inverse(), cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value, ) else: target_inv_inst = cls_or_self._target_inst.inverse() inverse_gate_class = type(target_inv_inst) params = target_inv_inst.arg_value inverse_gate = get_controlled_gate( inverse_gate_class, cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value, )(*params) return inverse_gate @classmethod def __str__(cls) -> str: return f"Gate({cls.name}, target_gate={cls.target_gate}, num_ctrl_qubits={cls.num_ctrl_qubits}, control_value={cls.ctrl_value})" def __eq__(self, other) -> bool: # Returns false for CRX(0.5), CRY(0.5) if type(self) is not type(other): return False # Returns false for CRX(0.5), CRX(0.6) if self.is_parametric and self._target_inst != other._target_inst: return False return True def __hash__(self) -> int: if self.is_parametric: return hash((type(self), self._target_inst)) return hash(type(self))
[docs] def get_controlled_gate( gate: Type[Gate], n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, gate_namespace: NameSpace | None = None, ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ if control_value is None: control_value = (1 << n_ctrl_qubits) - 1 if gate_name is None: gate_name = f"{'C' * n_ctrl_qubits}{gate.name}" if gate_namespace is not None: found_gate = gate_namespace.get((gate.name, n_ctrl_qubits, control_value)) if found_gate is not None: warnings.warn( f"Found the same existing Controlled Gate {found_gate.name}", UserWarning, ) return found_gate class _CustomControlledGate(ControlledGate): __slots__ = () namespace = gate_namespace name = gate_name latex_str = rf"{gate_name}" num_ctrl_qubits = n_ctrl_qubits num_qubits = n_ctrl_qubits + gate.num_qubits ctrl_value = control_value target_gate = gate return _CustomControlledGate