From a0922c618b5f22197469609456b94e415c15ff23 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 13:18:37 +0530 Subject: [PATCH 001/117] Added Singleton design patter for non-parametric, non-controlled gates --- src/qutip_qip/operations/gateclass.py | 53 +++++++++++++++++++-------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 121ea4aa2..24cb50e3b 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -7,31 +7,54 @@ class _GateMetaClass(ABCMeta): - """ - The purpose of this meta class is to enforce read-only constraints on specific class attributes. - - This meta class 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). - """ _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] _read_only_set = set(_read_only) + _instances = {} + 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). + """ for attribute in cls._read_only_set: if name == attribute and hasattr(cls, attribute): raise AttributeError(f"{attribute} is read-only!") super().__setattr__(name, value) + def __call__(cls, *args, **kwargs): + """ + This is meant to add Singleton Design for Gates without any args + like arg_value, control_value etc. + + So creating X() 10,000 times (e.g., for a large circuit) becomes instant + because it's just a dictionary lookup after the first time. Also in memory + we only need to store one copy of X gate, no matter how many times it appears + in the circuit. + """ + + # For RX(0.5), RX(0.1) we want different instances. + # Same for CX(control_value=0), CX(control_value=1) + if cls.is_parametric_gate() or cls.is_controlled_gate(): + return super().__call__(*args, **kwargs) + + # For non-parametric gates (like X, H, Z), only one instance + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + + return cls._instances[cls] + class Gate(ABC, metaclass=_GateMetaClass): r""" From 48092e0c011d28313ea71622c6670c51b49dad05 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 13:58:16 +0530 Subject: [PATCH 002/117] Make validate_control, validate_params as staticmethod, corrected some typehints --- src/qutip_qip/operations/gateclass.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 24cb50e3b..d8cc3d1bd 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -1,3 +1,4 @@ +from __future__ import annotations # This won't be needed after minimum version becomes 3.14 (PEP 749) from abc import ABC, ABCMeta, abstractmethod import inspect @@ -56,6 +57,8 @@ def __call__(cls, *args, **kwargs): return cls._instances[cls] + + class Gate(ABC, metaclass=_GateMetaClass): r""" Abstract base class for a quantum gate. @@ -128,7 +131,7 @@ def __init_subclass__(cls, **kwargs): @property @abstractmethod - def num_qubits(self) -> Qobj: + def num_qubits(self) -> int: pass @staticmethod @@ -153,7 +156,7 @@ def is_clifford(self) -> bool: def self_inverse(self) -> bool: pass - def inverse(self): + def inverse(self) -> Gate: """ Return the inverse of the gate. @@ -269,8 +272,9 @@ def __init__(self, arg_value: float, arg_label: str | None = None): def num_params(self) -> Qobj: pass + @staticmethod @abstractmethod - def validate_params(self, arg_value): + def validate_params(arg_value): r""" Validate the provided parameters. @@ -324,7 +328,7 @@ class ControlledGate(Gate): * If the gate should execute when the 0-th qubit is $|1\rangle$, set ``control_value=1``. * If the gate should execute when two control qubits are $|10\rangle$ - (binary 10), set ``control_value=2``. + (binary 10), set ``control_value=0b10``. Defaults to all-ones (e.g., $2^N - 1$) if not provided. @@ -404,7 +408,8 @@ def self_inverse(self) -> int: def control_value(self) -> int: return self._control_value - def _validate_control_value(self, control_value: int) -> None: + @classmethod + def _validate_control_value(cls, control_value: int) -> None: """ Internal validation for the control value. @@ -423,7 +428,7 @@ def _validate_control_value(self, control_value: int) -> None: if control_value < 0: raise ValueError(f"Control value can't be negative, got {control_value}") - if control_value > 2**self.num_ctrl_qubits - 1: + if control_value > 2**cls.num_ctrl_qubits - 1: raise ValueError(f"Control value can't be greater than 2^num_ctrl_qubits - 1, got {control_value}") def get_qobj(self) -> Qobj: @@ -506,13 +511,12 @@ def control_value(self) -> int: class AngleParametricGate(ParametricGate): - def validate_params(self, arg_value): + self_inverse = False + + @staticmethod + def validate_params(arg_value): for arg in arg_value: try: float(arg) except TypeError: raise ValueError(f"Invalid arg {arg} in arg_value") - - @property - def self_inverse(self) -> int: - return False From be354e7e7fb769dd187c895857579373d0fd14df Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 14:32:02 +0530 Subject: [PATCH 003/117] Moved certain abstract properties as class attribute in Gate class --- src/qutip_qip/operations/gateclass.py | 40 ++++++++------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index d8cc3d1bd..736c5f40f 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -8,7 +8,6 @@ class _GateMetaClass(ABCMeta): - _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] _read_only_set = set(_read_only) @@ -47,7 +46,7 @@ def __call__(cls, *args, **kwargs): # For RX(0.5), RX(0.1) we want different instances. # Same for CX(control_value=0), CX(control_value=1) - if cls.is_parametric_gate() or cls.is_controlled_gate(): + if args or kwargs: return super().__call__(*args, **kwargs) # For non-parametric gates (like X, H, Z), only one instance @@ -57,8 +56,6 @@ def __call__(cls, *args, **kwargs): return cls._instances[cls] - - class Gate(ABC, metaclass=_GateMetaClass): r""" Abstract base class for a quantum gate. @@ -79,6 +76,7 @@ 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 @@ -89,6 +87,10 @@ class attribute for subclasses. Defaults to the class name if not provided. """ + num_qubits: int | None = None + self_inverse: bool = False + is_clifford: bool = False + def __init_subclass__(cls, **kwargs): """ Automatically runs when a new subclass is defined via inheritance. @@ -129,11 +131,6 @@ def __init_subclass__(cls, **kwargs): f"got {type(num_qubits)} with value {num_qubits}." ) - @property - @abstractmethod - def num_qubits(self) -> int: - pass - @staticmethod @abstractmethod def get_qobj() -> Qobj: @@ -147,15 +144,6 @@ def get_qobj() -> Qobj: """ pass - @property - def is_clifford(self) -> bool: - return False - - @property - @abstractmethod - def self_inverse(self) -> bool: - pass - def inverse(self) -> Gate: """ Return the inverse of the gate. @@ -238,6 +226,8 @@ class attribute for subclasses. If the number of provided arguments does not match `num_params`. """ + num_params: int | None = None + def __init_subclass__(cls, **kwargs) -> None: """ Validates the subclass definition. @@ -267,11 +257,6 @@ def __init__(self, arg_value: float, arg_label: str | None = None): self.arg_value = arg_value self.arg_label = arg_label - @property - @abstractmethod - def num_params(self) -> Qobj: - pass - @staticmethod @abstractmethod def validate_params(arg_value): @@ -341,6 +326,8 @@ class ControlledGate(Gate): The gate to be applied to the target qubits. """ + num_ctrl_qubits: int | None = None + def __init_subclass__(cls, **kwargs): """ Validates the subclass definition. @@ -392,12 +379,7 @@ def __init__( @property @abstractmethod - def num_ctrl_qubits() -> int: - pass - - @property - @abstractmethod - def target_gate() -> int: + def target_gate() -> Gate: pass @property From cee446d0c13de9a6cf1146273ec07000eb5c65ab Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 15:19:18 +0530 Subject: [PATCH 004/117] Deprecated gates.py --- src/qutip_qip/operations/__init__.py | 8 +- src/qutip_qip/operations/gates.py | 306 +-------------------------- src/qutip_qip/operations/utils.py | 298 ++++++++++++++++++++++++++ tests/test_gates.py | 16 +- 4 files changed, 316 insertions(+), 312 deletions(-) create mode 100644 src/qutip_qip/operations/utils.py diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index e9c930079..b5e39304b 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -2,6 +2,11 @@ Operations on quantum circuits. """ +from .utils import ( + expand_operator, + gate_sequence_product, + controlled_gate, +) from .gates import ( rx, ry, @@ -33,12 +38,9 @@ molmer_sorensen, toffoli, rotation, - controlled_gate, globalphase, hadamard_transform, qubit_clifford_group, - expand_operator, - gate_sequence_product, ) from .gateclass import ( diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/gates.py index 5a606e655..291749afd 100644 --- a/src/qutip_qip/operations/gates.py +++ b/src/qutip_qip/operations/gates.py @@ -1,5 +1,7 @@ -import numbers -from collections.abc import Iterable +""" +Deprecated module, will be removed in future versions. +""" + from itertools import product from functools import partial, reduce from operator import mul @@ -8,9 +10,8 @@ import numpy as np import scipy.sparse as sp -import qutip from qutip import Qobj, identity, qeye, sigmax, sigmay, sigmaz, tensor, fock_dm - +from qutip_qip.operations import expand_operator # Single Qubit Gates def _deprecation_warnings_gate_expansion(): @@ -1078,61 +1079,6 @@ def rotation(op, phi, N=None, target=0): return (-1j * op * phi / 2).expm() -def controlled_gate( - U, - controls=0, - targets=1, - N=None, - control_value=1, -): - """ - Create an N-qubit controlled gate from a single-qubit gate U with the given - control and target qubits. - - Parameters - ---------- - U : :class:`qutip.Qobj` - An arbitrary unitary gate. - controls : list of int - The index of the first control qubit. - targets : list of int - The index of the target qubit. - N : int - The total number of qubits. - control_value : int - The decimal value of the controlled qubits that activates the gate U. - - Returns - ------- - result : qobj - Quantum object representing the controlled-U gate. - """ - # Compatibility - if not isinstance(controls, Iterable): - controls = [controls] - if not isinstance(targets, Iterable): - targets = [targets] - num_controls = len(controls) - num_targets = len(U.dims[0]) - N = num_controls + num_targets if N is None else N - - # First, assume that the last qubit is the target and control qubits are - # in the increasing order. - # The control_value is the location of this unitary. - block_matrices = [np.array([[1, 0], [0, 1]])] * 2**num_controls - block_matrices[control_value] = U.full() - from scipy.linalg import block_diag # move this to the top of the file - - result = block_diag(*block_matrices) - result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) - - # Expand it to N qubits and permute qubits labelling - if controls + targets == list(range(N)): - return result - else: - return expand_operator(result, N, targets=controls + targets) - - def globalphase(theta, N=1): """ Returns quantum object representing the global phase shift gate. @@ -1269,245 +1215,3 @@ def qubit_clifford_group(N=None, target=0): yield expand_operator(op, N, target) else: yield op - - -# -# Gate Expand -# - - -def _check_oper_dims(oper, dims=None, targets=None): - """ - Check if the given operator is valid. - - Parameters - ---------- - oper : :class:`qutip.Qobj` - The quantum object to be checked. - dims : list, optional - A list of integer for the dimension of each composite system. - e.g ``[2, 2, 2, 2, 2]`` for 5 qubits system. - targets : int or list of int, optional - The indices of subspace that are acted on. - """ - # if operator matches N - if not isinstance(oper, Qobj) or oper.dims[0] != oper.dims[1]: - raise ValueError( - "The operator is not an " - "Qobj with the same input and output dimensions." - ) - # if operator dims matches the target dims - if dims is not None and targets is not None: - targ_dims = [dims[t] for t in targets] - if oper.dims[0] != targ_dims: - raise ValueError( - "The operator dims {} do not match " - "the target dims {}.".format(oper.dims[0], targ_dims) - ) - - -def _targets_to_list(targets, oper=None, N=None): - """ - transform targets to a list and check validity. - - Parameters - ---------- - targets : int or list of int - The indices of subspace that are acted on. - oper : :class:`qutip.Qobj`, optional - An operator, the type of the :class:`qutip.Qobj` - has to be an operator - and the dimension matches the tensored qubit Hilbert space - e.g. dims = ``[[2, 2, 2], [2, 2, 2]]`` - N : int, optional - The number of subspace in the system. - """ - # if targets is a list of integer - if targets is None: - targets = list(range(len(oper.dims[0]))) - if not hasattr(targets, "__iter__"): - targets = [targets] - if not all([isinstance(t, numbers.Integral) for t in targets]): - raise TypeError("targets should be an integer or a list of integer") - # if targets has correct length - if oper is not None: - req_num = len(oper.dims[0]) - if len(targets) != req_num: - raise ValueError( - "The given operator needs {} " - "target qutbis, " - "but {} given.".format(req_num, len(targets)) - ) - # if targets is smaller than N - if N is not None: - if not all([t < N for t in targets]): - raise ValueError("Targets must be smaller than N={}.".format(N)) - return targets - - -def expand_operator( - oper, N=None, targets=None, dims=None, cyclic_permutation=False, dtype=None -): - """ - Expand an operator to one that acts on a system with desired dimensions. - - Parameters - ---------- - oper : :class:`qutip.Qobj` - An operator that act on the subsystem, has to be an operator and the - dimension matches the tensored dims Hilbert space - e.g. oper.dims = ``[[2, 3], [2, 3]]`` - dims : list - A list of integer for the dimension of each composite system. - E.g ``[2, 3, 2, 3, 4]``. - targets : int or list of int - The indices of subspace that are acted on. - Permutation can also be realized by changing the orders of the indices. - N : int - Deprecated. Number of qubits. Please use `dims`. - cyclic_permutation : boolean, optional - Deprecated. - Expand for all cyclic permutation of the targets. - E.g. if ``N=3`` and `oper` is a 2-qubit operator, - the result will be a list of three operators, - each acting on qubits 0 and 1, 1 and 2, 2 and 0. - dtype : str, optional - Data type of the output `Qobj`. Only for qutip version larger than 5. - - - Returns - ------- - expanded_oper : :class:`qutip.Qobj` - The expanded operator acting on a system with desired dimension. - - Examples - -------- - >>> from qutip_qip.operations import expand_operator, X, CNOT - >>> import qutip - >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True - Qobj data = - [[0. 0. 0. 1. 0. 0.] - [0. 0. 0. 0. 1. 0.] - [0. 0. 0. 0. 0. 1.] - [1. 0. 0. 0. 0. 0.] - [0. 1. 0. 0. 0. 0.] - [0. 0. 1. 0. 0. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True - Qobj data = - [[1. 0. 0. 0. 0. 0. 0. 0.] - [0. 1. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 1. 0. 0. 0. 0.] - [0. 0. 1. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 1. 0. 0. 0.] - [0. 0. 0. 0. 0. 1. 0. 0.] - [0. 0. 0. 0. 0. 0. 0. 1.] - [0. 0. 0. 0. 0. 0. 1. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True - Qobj data = - [[1. 0. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 1. 0. 0.] - [0. 0. 1. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 0. 0. 1.] - [0. 0. 0. 0. 1. 0. 0. 0.] - [0. 1. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 0. 1. 0.] - [0. 0. 0. 1. 0. 0. 0. 0.]] - """ - dtype = dtype or qutip.settings.core["default_dtype"] or qutip.data.CSR - oper = oper.to(dtype) - - if N is not None: - warnings.warn( - "The function expand_operator has been generalized to " - "arbitrary subsystems instead of only qubit systems." - "Please use the new signature e.g.\n" - "expand_operator(oper, dims=[2, 3, 2, 2], targets=2)", - DeprecationWarning, - stacklevel=2 - ) - - if dims is not None and N is None: - if not isinstance(dims, Iterable): - f"dims needs to be an interable {not type(dims)}." - N = len(dims) # backward compatibility - - if dims is None: - dims = [2] * N - targets = _targets_to_list(targets, oper=oper, N=N) - _check_oper_dims(oper, dims=dims, targets=targets) - - # Call expand_operator for all cyclic permutation of the targets. - if cyclic_permutation: - warnings.warn( - "cyclic_permutation is deprecated, " - "please use loop through different targets manually.", - DeprecationWarning, - stacklevel=2 - ) - oper_list = [] - for i in range(N): - new_targets = np.mod(np.array(targets) + i, N) - oper_list.append( - expand_operator(oper, N=N, targets=new_targets, dims=dims) - ) - return oper_list - - # Generate the correct order for permutation, - # eg. if N = 5, targets = [3,0], the order is [1,2,3,0,4]. - # If the operator is cnot, - # this order means that the 3rd qubit controls the 0th qubit. - new_order = [0] * N - for i, t in enumerate(targets): - new_order[t] = i - # allocate the rest qutbits (not targets) to the empty - # position in new_order - rest_pos = [q for q in list(range(N)) if q not in targets] - rest_qubits = list(range(len(targets), N)) - for i, ind in enumerate(rest_pos): - new_order[ind] = rest_qubits[i] - id_list = [identity(dims[i]) for i in rest_pos] - out = tensor([oper] + id_list).permute(new_order) - return out.to(dtype) - - -def gate_sequence_product( - U_list, left_to_right=True, inds_list=None, expand=False -): - """ - Calculate the overall unitary matrix for a given list of unitary operations. - - Parameters - ---------- - U_list: list - List of gates implementing the quantum circuit. - - left_to_right: Boolean, optional - Check if multiplication is to be done from left to right. - - inds_list: list of list of int, optional - If expand=True, list of qubit indices corresponding to U_list - to which each unitary is applied. - - expand: Boolean, optional - Check if the list of unitaries need to be expanded to full dimension. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - - overall_inds : list of int, optional - List of qubit indices on which U_overall applies. - """ - from qutip_qip.circuit.simulator import ( - gate_sequence_product, - gate_sequence_product_with_expansion, - ) - - if expand: - return gate_sequence_product(U_list, inds_list) - else: - return gate_sequence_product_with_expansion(U_list, left_to_right) diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py new file mode 100644 index 000000000..4cb947bde --- /dev/null +++ b/src/qutip_qip/operations/utils.py @@ -0,0 +1,298 @@ +import warnings +import numbers +from collections.abc import Iterable + +import numpy as np +import qutip +from qutip import Qobj, identity, tensor + +def _check_oper_dims(oper: Qobj, dims=None, targets=None): + """ + Check if the given operator is valid. + + Parameters + ---------- + oper : :class:`qutip.Qobj` + The quantum object to be checked. + dims : list, optional + A list of integer for the dimension of each composite system. + e.g ``[2, 2, 2, 2, 2]`` for 5 qubits system. + targets : int or list of int, optional + The indices of subspace that are acted on. + """ + # If operator matches N + if not isinstance(oper, Qobj) or oper.dims[0] != oper.dims[1]: + raise ValueError( + "The operator is not an " + "Qobj with the same input and output dimensions." + ) + # if operator dims matches the target dims + if dims is not None and targets is not None: + targ_dims = [dims[t] for t in targets] + if oper.dims[0] != targ_dims: + raise ValueError( + f"The operator dims {oper.dims[0]} do not match " + "the target dims {targ_dims}." + ) + + +def _targets_to_list(targets, oper=None, N=None): + """ + transform targets to a list and check validity. + + Parameters + ---------- + targets : int or list of int + The indices of subspace that are acted on. + oper : :class:`qutip.Qobj`, optional + An operator, the type of the :class:`qutip.Qobj` + has to be an operator + and the dimension matches the tensored qubit Hilbert space + e.g. dims = ``[[2, 2, 2], [2, 2, 2]]`` + N : int, optional + The number of subspace in the system. + """ + # if targets is a list of integer + if targets is None: + targets = list(range(len(oper.dims[0]))) + if not hasattr(targets, "__iter__"): + targets = [targets] + if not all([isinstance(t, numbers.Integral) for t in targets]): + raise TypeError("targets should be an integer or a list of integer") + # if targets has correct length + if oper is not None: + req_num = len(oper.dims[0]) + if len(targets) != req_num: + raise ValueError( + f"The given operator needs {req_num} " + "target qutbis, " + "but {len(targets)} given." + ) + + # If targets is smaller than N + if N is not None: + if not all([t < N for t in targets]): + raise ValueError("Targets must be smaller than N={}.".format(N)) + return targets + +def expand_operator( + oper, N=None, targets=None, dims=None, cyclic_permutation=False, dtype=None +): + """ + Expand an operator to one that acts on a system with desired dimensions. + + Parameters + ---------- + oper : :class:`qutip.Qobj` + An operator that act on the subsystem, has to be an operator and the + dimension matches the tensored dims Hilbert space + e.g. oper.dims = ``[[2, 3], [2, 3]]`` + dims : list + A list of integer for the dimension of each composite system. + E.g ``[2, 3, 2, 3, 4]``. + targets : int or list of int + The indices of subspace that are acted on. + Permutation can also be realized by changing the orders of the indices. + N : int + Deprecated. Number of qubits. Please use `dims`. + cyclic_permutation : boolean, optional + Deprecated. + Expand for all cyclic permutation of the targets. + E.g. if ``N=3`` and `oper` is a 2-qubit operator, + the result will be a list of three operators, + each acting on qubits 0 and 1, 1 and 2, 2 and 0. + dtype : str, optional + Data type of the output `Qobj`. Only for qutip version larger than 5. + + + Returns + ------- + expanded_oper : :class:`qutip.Qobj` + The expanded operator acting on a system with desired dimension. + + Examples + -------- + >>> from qutip_qip.operations import expand_operator, X, CNOT + >>> import qutip + >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True + Qobj data = + [[0. 0. 0. 1. 0. 0.] + [0. 0. 0. 0. 1. 0.] + [0. 0. 0. 0. 0. 1.] + [1. 0. 0. 0. 0. 0.] + [0. 1. 0. 0. 0. 0.] + [0. 0. 1. 0. 0. 0.]] + >>> expand_operator(CNOT.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True + Qobj data = + [[1. 0. 0. 0. 0. 0. 0. 0.] + [0. 1. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 1. 0. 0. 0. 0.] + [0. 0. 1. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 1. 0. 0. 0.] + [0. 0. 0. 0. 0. 1. 0. 0.] + [0. 0. 0. 0. 0. 0. 0. 1.] + [0. 0. 0. 0. 0. 0. 1. 0.]] + >>> expand_operator(CNOT.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True + Qobj data = + [[1. 0. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 1. 0. 0.] + [0. 0. 1. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 0. 0. 1.] + [0. 0. 0. 0. 1. 0. 0. 0.] + [0. 1. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 0. 1. 0.] + [0. 0. 0. 1. 0. 0. 0. 0.]] + """ + dtype = dtype or qutip.settings.core["default_dtype"] or qutip.data.CSR + oper = oper.to(dtype) + + if N is not None: + warnings.warn( + "The function expand_operator has been generalized to " + "arbitrary subsystems instead of only qubit systems." + "Please use the new signature e.g.\n" + "expand_operator(oper, dims=[2, 3, 2, 2], targets=2)", + DeprecationWarning, + stacklevel=2 + ) + + if dims is not None and N is None: + if not isinstance(dims, Iterable): + f"dims needs to be an interable {not type(dims)}." + N = len(dims) # backward compatibility + + if dims is None: + dims = [2] * N + targets = _targets_to_list(targets, oper=oper, N=N) + _check_oper_dims(oper, dims=dims, targets=targets) + + # Call expand_operator for all cyclic permutation of the targets. + if cyclic_permutation: + warnings.warn( + "cyclic_permutation is deprecated, " + "please use loop through different targets manually.", + DeprecationWarning, + stacklevel=2 + ) + oper_list = [] + for i in range(N): + new_targets = np.mod(np.array(targets) + i, N) + oper_list.append( + expand_operator(oper, N=N, targets=new_targets, dims=dims) + ) + return oper_list + + # Generate the correct order for permutation, + # eg. if N = 5, targets = [3,0], the order is [1,2,3,0,4]. + # If the operator is cnot, + # this order means that the 3rd qubit controls the 0th qubit. + new_order = [0] * N + for i, t in enumerate(targets): + new_order[t] = i + # allocate the rest qutbits (not targets) to the empty + # position in new_order + rest_pos = [q for q in list(range(N)) if q not in targets] + rest_qubits = list(range(len(targets), N)) + for i, ind in enumerate(rest_pos): + new_order[ind] = rest_qubits[i] + id_list = [identity(dims[i]) for i in rest_pos] + out = tensor([oper] + id_list).permute(new_order) + return out.to(dtype) + + +def gate_sequence_product( + U_list, left_to_right=True, inds_list=None, expand=False +): + """ + Calculate the overall unitary matrix for a given list of unitary operations. + + Parameters + ---------- + U_list: list + List of gates implementing the quantum circuit. + + left_to_right: Boolean, optional + Check if multiplication is to be done from left to right. + + inds_list: list of list of int, optional + If expand=True, list of qubit indices corresponding to U_list + to which each unitary is applied. + + expand: Boolean, optional + Check if the list of unitaries need to be expanded to full dimension. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + + overall_inds : list of int, optional + List of qubit indices on which U_overall applies. + """ + from qutip_qip.circuit.simulator import ( + gate_sequence_product, + gate_sequence_product_with_expansion, + ) + + if expand: + return gate_sequence_product(U_list, inds_list) + else: + return gate_sequence_product_with_expansion(U_list, left_to_right) + + +def controlled_gate( + U, + controls=0, + targets=1, + N=None, + control_value=1, +): + """ + Create an N-qubit controlled gate from a single-qubit gate U with the given + control and target qubits. + + Parameters + ---------- + U : :class:`qutip.Qobj` + An arbitrary unitary gate. + controls : list of int + The index of the first control qubit. + targets : list of int + The index of the target qubit. + N : int + The total number of qubits. + control_value : int + The decimal value of the controlled qubits that activates the gate U. + + Returns + ------- + result : qobj + Quantum object representing the controlled-U gate. + """ + # Compatibility + if not isinstance(controls, Iterable): + controls = [controls] + if not isinstance(targets, Iterable): + targets = [targets] + num_controls = len(controls) + num_targets = len(U.dims[0]) + N = num_controls + num_targets if N is None else N + + # First, assume that the last qubit is the target and control qubits are + # in the increasing order. + # The control_value is the location of this unitary. + block_matrices = [np.array([[1, 0], [0, 1]])] * 2**num_controls + block_matrices[control_value] = U.full() + from scipy.linalg import block_diag # move this to the top of the file + + result = block_diag(*block_matrices) + result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) + + # Expand it to N qubits and permute qubits labelling + if controls + targets == list(range(N)): + return result + else: + return expand_operator(result, N, targets=controls + targets) diff --git a/tests/test_gates.py b/tests/test_gates.py index e8eccfb7b..6d1553def 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -3,7 +3,7 @@ import itertools import numpy as np import qutip -from qutip_qip.operations import gates +from qutip.core.gates import hadamard_transform, qubit_clifford_group from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import Gate, expand_operator import qutip_qip.operations.std as std @@ -154,7 +154,7 @@ class TestCliffordGroup: group for a single qubit. """ - clifford = list(gates.qubit_clifford_group()) + clifford = list(qubit_clifford_group()) pauli = [qutip.qeye(2), qutip.sigmax(), qutip.sigmay(), qutip.sigmaz()] def test_single_qubit_group_dimension_is_24(self): @@ -168,7 +168,7 @@ def test_all_elements_different(self): fid = qutip.average_gate_fidelity(gate, other) assert not np.allclose(fid, 1.0, atol=1e-3) - @pytest.mark.parametrize("gate", gates.qubit_clifford_group()) + @pytest.mark.parametrize("gate", qubit_clifford_group()) def test_gate_normalises_pauli_group(self, gate): """ Test the fundamental definition of the Clifford group, i.e. that it @@ -308,7 +308,7 @@ class Test_expand_operator: ) def test_permutation_without_expansion(self, permutation): base = qutip.tensor([qutip.rand_unitary(2) for _ in permutation]) - test = gates.expand_operator( + test = expand_operator( base, dims=[2] * len(permutation), targets=permutation ) expected = base.permute(_apply_permutation(permutation)) @@ -324,7 +324,7 @@ def test_general_qubit_expansion(self, n_targets): expected = _tensor_with_entanglement( [qutip.qeye(2)] * n_qubits, operation, targets ) - test = gates.expand_operator( + test = expand_operator( operation, dims=[2] * n_qubits, targets=targets ) np.testing.assert_allclose( @@ -332,7 +332,7 @@ def test_general_qubit_expansion(self, n_targets): ) def test_cnot_explicit(self): - test = gates.expand_operator( + test = expand_operator( std.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] ).full() expected = np.array( @@ -350,7 +350,7 @@ def test_cnot_explicit(self): np.testing.assert_allclose(test, expected, atol=1e-15) def test_hadamard_explicit(self): - test = gates.hadamard_transform(3).full() + test = hadamard_transform(3).full() expected = np.array( [ [1, 1, 1, 1, 1, 1, 1, 1], @@ -387,7 +387,7 @@ def test_non_qubit_systems(self, dimensions): ] expected = qutip.tensor(*operators) base_test = qutip.tensor(*[operators[x] for x in targets]) - test = gates.expand_operator( + test = expand_operator( base_test, dims=dimensions, targets=targets ) assert test.dims == expected.dims From 0d45d1d0524a6b06d0059153757bf395ed5e6c69 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 16:40:02 +0530 Subject: [PATCH 005/117] Added __slots__ for optimization --- src/qutip_qip/operations/gateclass.py | 31 ++++-- src/qutip_qip/operations/gates.py | 12 --- src/qutip_qip/operations/std/__init__.py | 3 +- src/qutip_qip/operations/std/other_gates.py | 7 +- .../operations/std/single_qubit_gate.py | 51 +++++++++- .../operations/std/two_qubit_gate.py | 94 ++++++++++++------- src/qutip_qip/operations/utils.py | 24 +++-- 7 files changed, 152 insertions(+), 70 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 736c5f40f..dd3cefc08 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -86,11 +86,18 @@ class attribute for subclasses. 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__ = () num_qubits: int | None = None self_inverse: bool = False is_clifford: bool = False + name = None + latex_str = None + def __init_subclass__(cls, **kwargs): """ Automatically runs when a new subclass is defined via inheritance. @@ -117,10 +124,11 @@ def __init_subclass__(cls, **kwargs): # print(H.name) -> 'Hadamard' if "name" not in cls.__dict__: + # This __dict__ belongs to the class, __slots__ removes __dict__ from object instances cls.name = cls.__name__ # Same as above for attribute latex_str (used in circuit draw) - if "latex_str" not in cls.__dict__: + if "latex_str" not in cls.__dict__: cls.latex_str = cls.__name__ # Assert num_qubits is a non-negative integer @@ -225,6 +233,7 @@ class attribute for subclasses. ValueError If the number of provided arguments does not match `num_params`. """ + __slots__ = ('arg_value', 'arg_label') num_params: int | None = None @@ -325,7 +334,7 @@ class ControlledGate(Gate): target_gate : Gate The gate to be applied to the target qubits. """ - + __slots__ = ('arg_value', 'arg_label') num_ctrl_qubits: int | None = None def __init_subclass__(cls, **kwargs): @@ -350,7 +359,7 @@ def __init_subclass__(cls, **kwargs): f"got {type(num_ctrl_qubits)} with value {num_ctrl_qubits}." ) - if cls.num_ctrl_qubits >= cls.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'") # Assert num_ctrl_qubits + target_gate.num_qubits = num_qubits @@ -360,6 +369,14 @@ def __init_subclass__(cls, **kwargs): # Default value for control_value cls._control_value = 2**cls.num_ctrl_qubits - 1 + # Automatically copy the validator from the target + if hasattr(cls, "target_gate") and hasattr(cls.target_gate, "validate_params"): + cls.validate_params = staticmethod(cls.target_gate.validate_params) + + # In the circuit plot, only the target gate is shown. + # The control has its own symbol. + cls.latex_str = cls.target_gate.latex_str + def __init__( self, arg_value: any = None, @@ -373,10 +390,6 @@ def __init__( if self.is_parametric_gate(): ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) - # In the circuit plot, only the target gate is shown. - # The control has its own symbol. - self.latex_str = self.target_gate.latex_str - @property @abstractmethod def target_gate() -> Gate: @@ -493,8 +506,8 @@ def control_value(self) -> int: class AngleParametricGate(ParametricGate): - self_inverse = False - + __slots__ = () + @staticmethod def validate_params(arg_value): for arg in arg_value: diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/gates.py index 291749afd..1d57acf27 100644 --- a/src/qutip_qip/operations/gates.py +++ b/src/qutip_qip/operations/gates.py @@ -1120,18 +1120,6 @@ def globalphase(theta, N=1): # -def _hamming_distance(x, bits=32): - """ - Calculate the bit-wise Hamming distance of x from 0: That is, the number - 1s in the integer x. - """ - tot = 0 - while x: - tot += 1 - x &= x - 1 - return tot - - def hadamard_transform(N=1): """Quantum object representing the N-qubit Hadamard gate. diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/std/__init__.py index 1cb59de6e..acd844d5c 100644 --- a/src/qutip_qip/operations/std/__init__.py +++ b/src/qutip_qip/operations/std/__init__.py @@ -44,7 +44,6 @@ TOFFOLI, FREDKIN, ) - __all__ = [ "IDLE", @@ -87,4 +86,4 @@ "GLOBALPHASE", "TOFFOLI", "FREDKIN", -] \ No newline at end of file +] diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/std/other_gates.py index 25a5ee258..900920260 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/std/other_gates.py @@ -19,6 +19,7 @@ class GLOBALPHASE(AngleParametricGate): num_params: int = 1 self_inverse = False latex_str = r"{\rm GLOBALPHASE}" + __slots__ = () def __init__(self, arg_value: float = 0.0): super().__init__(arg_value=arg_value) @@ -32,10 +33,12 @@ def get_qobj(self, num_qubits=None): N = 2**num_qubits return Qobj( - np.exp(1.0j * self.arg_value[0]) * sp.eye(N, N, dtype=complex, format="csr"), + np.exp(1.0j * self.arg_value[0]) + * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], ) + class TOFFOLI(ControlledGate): """ TOFFOLI gate. @@ -61,6 +64,7 @@ class TOFFOLI(ControlledGate): num_qubits: int = 3 num_ctrl_qubits: int = 2 + __slots__ = () @staticmethod def get_qobj() -> Qobj: @@ -104,6 +108,7 @@ class FREDKIN(ControlledGate): num_qubits: int = 3 num_ctrl_qubits: int = 1 + __slots__ = () @staticmethod def get_qobj() -> Qobj: diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 42cacfbbc..b16edece1 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -4,13 +4,18 @@ from qutip import Qobj, sigmax, sigmay, sigmaz, qeye from qutip_qip.operations import Gate, AngleParametricGate + class _SingleQubitGate(Gate): """Abstract one-qubit gate.""" + + __slots__ = () num_qubits = 1 class _SingleQubitParametricGate(AngleParametricGate): """Abstract one-qubit parametric gate.""" + + __slots__ = () num_qubits = 1 @@ -27,6 +32,9 @@ class X(_SingleQubitGate): [[0. 1.] [1. 0.]] """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"X" @@ -49,6 +57,9 @@ class Y(_SingleQubitGate): [[0.+0.j 0.-1.j] [0.+1.j 0.+0.j]] """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"Y" @@ -71,6 +82,9 @@ class Z(_SingleQubitGate): [[ 1. 0.] [ 0. -1.]] """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"Z" @@ -88,6 +102,9 @@ class IDLE(_SingleQubitGate): -------- >>> from qutip_qip.operations import IDLE """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"{\rm IDLE}" @@ -110,6 +127,9 @@ class H(_SingleQubitGate): [[ 0.70711 0.70711] [ 0.70711 -0.70711]] """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"H" @@ -120,6 +140,8 @@ def get_qobj(): class SNOT(H): + __slots__ = () + def __init__(self): warnings.warn( "SNOT is deprecated and will be removed in future versions. " @@ -143,6 +165,9 @@ class SQRTX(_SingleQubitGate): [[0.5+0.5j 0.5-0.5j] [0.5-0.5j 0.5+0.5j]] """ + + __slots__ = () + self_inverse = False is_clifford = True latex_str = r"\sqrt{\rm NOT}" @@ -153,6 +178,8 @@ def get_qobj(): class SQRTNOT(SQRTX): + __slots__ = () + def __init__(self): warnings.warn( "SQRTNOT is deprecated and will be removed in future versions. " @@ -176,6 +203,9 @@ class S(_SingleQubitGate): [[1.+0.j 0.+0.j] [0.+0.j 0.+1.j]] """ + + __slots__ = () + self_inverse = False is_clifford = True latex_str = r"{\rm S}" @@ -198,6 +228,9 @@ class T(_SingleQubitGate): [[1. +0.j 0. +0.j ] [0. +0.j 0.70711+0.70711j]] """ + + __slots__ = () + self_inverse = False latex_str = r"{\rm T}" @@ -219,6 +252,9 @@ class RX(_SingleQubitParametricGate): [[0.70711+0.j 0. -0.70711j] [0. -0.70711j 0.70711+0.j ]] """ + + __slots__ = () + num_params = 1 latex_str = r"R_x" @@ -229,7 +265,7 @@ def get_qobj(self): [np.cos(phi / 2), -1j * np.sin(phi / 2)], [-1j * np.sin(phi / 2), np.cos(phi / 2)], ], - dims = [[2], [2]] + dims=[[2], [2]], ) @@ -246,6 +282,9 @@ class RY(_SingleQubitParametricGate): [[ 0.70711 -0.70711] [ 0.70711 0.70711]] """ + + __slots__ = () + num_params = 1 latex_str = r"R_y" @@ -272,6 +311,9 @@ class RZ(_SingleQubitParametricGate): [[0.70711-0.70711j 0. +0.j ] [0. +0.j 0.70711+0.70711j]] """ + + __slots__ = () + num_params = 1 latex_str = r"R_z" @@ -288,6 +330,9 @@ class PHASE(_SingleQubitParametricGate): -------- >>> from qutip_qip.operations import PHASE """ + + __slots__ = () + num_params = 1 latex_str = r"PHASE" @@ -322,6 +367,8 @@ class R(_SingleQubitParametricGate): [ 0.70711 0.70711]] """ + __slots__ = () + num_params = 2 latex_str = r"{\rm R}" @@ -358,6 +405,8 @@ class QASMU(_SingleQubitParametricGate): [ 0.5+0.5j -0.5+0.5j]] """ + __slots__ = () + num_params = 3 latex_str = r"{\rm QASMU}" diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index fd28f3888..4ead4e62b 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -4,17 +4,14 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations import ( - Gate, - ControlledGate, - AngleParametricGate, -) -from qutip_qip.operations.std import ( - X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE -) +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate +from qutip_qip.operations.std import X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE + class _TwoQubitGate(Gate): """Abstract two-qubit gate.""" + + __slots__ = () num_qubits: Final[int] = 2 @@ -26,6 +23,7 @@ class _ControlledTwoQubitGate(ControlledGate): and raise an error if it is 0. """ + __slots__ = () num_qubits: Final[int] = 2 num_ctrl_qubits: Final[int] = 1 @@ -45,6 +43,9 @@ class SWAP(_TwoQubitGate): [0. 1. 0. 0.] [0. 0. 0. 1.]] """ + + __slots__ = () + self_inverse = True is_clifford = True latex_str = r"{\rm SWAP}" @@ -72,6 +73,9 @@ class ISWAP(_TwoQubitGate): [0.+0.j 0.+1.j 0.+0.j 0.+0.j] [0.+0.j 0.+0.j 0.+0.j 1.+0.j]] """ + + __slots__ = () + self_inverse = False is_clifford = True latex_str = r"{i}{\rm SWAP}" @@ -99,6 +103,9 @@ class SQRTSWAP(_TwoQubitGate): [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] """ + + __slots__ = () + self_inverse = False latex_str = r"\sqrt{\rm SWAP}" @@ -132,6 +139,9 @@ class SQRTISWAP(_TwoQubitGate): [0. +0.j 0. +0.70711j 0.70711+0.j 0. +0.j ] [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] """ + + __slots__ = () + self_inverse = False is_clifford = True latex_str = r"\sqrt{{i}\rm SWAP}" @@ -175,6 +185,9 @@ class BERKELEY(_TwoQubitGate): [0. +0.j 0. +0.92388j 0.38268+0.j 0. +0.j ] [0. +0.38268j 0. +0.j 0. +0.j 0.92388+0.j ]] """ + + __slots__ = () + self_inverse = False latex_str = r"{\rm BERKELEY}" @@ -216,6 +229,8 @@ class SWAPALPHA(AngleParametricGate): [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] """ + __slots__ = () + num_qubits = 2 num_params: int = 1 latex_str = r"{\rm SWAPALPHA}" @@ -268,6 +283,8 @@ class MS(AngleParametricGate): [0. -0.70711j 0. +0.j 0. +0.j 0.70711+0.j ]] """ + __slots__ = () + num_qubits = 2 num_params: int = 2 latex_str = r"{\rm MS}" @@ -320,6 +337,8 @@ class RZX(AngleParametricGate): [0.+0.j 0.+0.j 0.+1.j 0.+0.j]] """ + __slots__ = () + num_qubits = 2 num_params = 1 latex_str = r"{\rm RZX}" @@ -355,6 +374,8 @@ class CX(_ControlledTwoQubitGate): [0. 0. 1. 0.]] """ + __slots__ = () + target_gate = X is_clifford = True latex_str = r"{\rm CNOT}" @@ -368,6 +389,8 @@ def get_qobj(): class CNOT(CX): + __slots__ = () + def __init__(self, control_value=None): warnings.warn( "CNOT is deprecated and will be removed in future versions. " @@ -394,6 +417,8 @@ class CY(_ControlledTwoQubitGate): [ 0+0j. 0.+0j 0.+1j. 0.+0j]] """ + __slots__ = () + target_gate = Y is_clifford = True latex_str = r"{\rm CY}" @@ -422,6 +447,8 @@ class CZ(_ControlledTwoQubitGate): [ 0. 0. 0. -1.]] """ + __slots__ = () + target_gate = Z is_clifford = True latex_str = r"{\rm CZ}" @@ -435,6 +462,8 @@ def get_qobj(): class CSIGN(CZ): + __slots__ = () + def __init__(self): warnings.warn( "CSIGN is deprecated and will be removed in future versions. " @@ -463,6 +492,8 @@ class CH(_ControlledTwoQubitGate): >>> from qutip_qip.operations import CH """ + __slots__ = () + target_gate = H latex_str = r"{\rm CH}" @@ -498,6 +529,8 @@ class CT(_ControlledTwoQubitGate): >>> from qutip_qip.operations import CPHASE """ + __slots__ = () + target_gate = T latex_str = r"{\rm CT}" @@ -532,6 +565,8 @@ class CS(_ControlledTwoQubitGate): >>> from qutip_qip.operations import CPHASE """ + __slots__ = () + target_gate = S latex_str = r"{\rm CS}" @@ -545,19 +580,7 @@ def get_qobj(): ) -class _ControlledParamTwoQubitGate(ControlledGate, AngleParametricGate): - """ - This class allows correctly generating the gate instance - when a redundant control_value is given, e.g. - ``CNOT(0, 1, control_value=1)``, - and raise an error if it is 0. - """ - - num_qubits: Final[int] = 2 - num_ctrl_qubits: Final[int] = 1 - - -class CPHASE(_ControlledParamTwoQubitGate): +class CPHASE(_ControlledTwoQubitGate): r""" CPHASE gate. @@ -582,23 +605,14 @@ class CPHASE(_ControlledParamTwoQubitGate): [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] """ + __slots__ = () + num_params: int = 1 target_gate = PHASE latex_str = r"{\rm CPHASE}" - def get_qobj(self): - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, np.exp(1j * self.arg_value[0])], - ], - dims=[[2, 2], [2, 2]], - ) - -class CRX(_ControlledParamTwoQubitGate): +class CRX(_ControlledTwoQubitGate): r""" Controlled X rotation. @@ -607,12 +621,14 @@ class CRX(_ControlledParamTwoQubitGate): >>> from qutip_qip.operations import CRX """ + __slots__ = () + num_params: int = 1 target_gate = RX latex_str = r"{\rm CRX}" -class CRY(_ControlledParamTwoQubitGate): +class CRY(_ControlledTwoQubitGate): r""" Controlled Y rotation. @@ -621,12 +637,14 @@ class CRY(_ControlledParamTwoQubitGate): >>> from qutip_qip.operations import CRY """ + __slots__ = () + latex_str = r"{\rm CRY}" target_gate = RY num_params: int = 1 -class CRZ(_ControlledParamTwoQubitGate): +class CRZ(_ControlledTwoQubitGate): r""" CRZ gate. @@ -651,12 +669,14 @@ class CRZ(_ControlledParamTwoQubitGate): [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] """ + __slots__ = () + num_params: int = 1 target_gate = RZ latex_str = r"{\rm CRZ}" -class CQASMU(_ControlledParamTwoQubitGate): +class CQASMU(_ControlledTwoQubitGate): r""" Controlled QASMU rotation. @@ -665,6 +685,8 @@ class CQASMU(_ControlledParamTwoQubitGate): >>> from qutip_qip.operations import CQASMU """ + __slots__ = () + num_params: int = 3 target_gate = QASMU latex_str = r"{\rm CQASMU}" diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 4cb947bde..de96c2e6a 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -1,8 +1,10 @@ import warnings import numbers +from typing import Sequence from collections.abc import Iterable import numpy as np +from scipy.linalg import block_diag import qutip from qutip import Qobj, identity, tensor @@ -76,7 +78,12 @@ def _targets_to_list(targets, oper=None, N=None): return targets def expand_operator( - oper, N=None, targets=None, dims=None, cyclic_permutation=False, dtype=None + oper: Qobj, + N: None = None, + targets: int | list[int] | None = None, + dims: list[int] | None = None, + cyclic_permutation: bool = False, + dtype: str | None = None ): """ Expand an operator to one that acts on a system with desired dimensions. @@ -244,12 +251,12 @@ def gate_sequence_product( def controlled_gate( - U, - controls=0, - targets=1, - N=None, - control_value=1, -): + U: Qobj, + controls: int | Sequence[int] = 0, + targets: int | Sequence[int] = 1, + N: int | None = None, + control_value: int = 1, +) -> Qobj: """ Create an N-qubit controlled gate from a single-qubit gate U with the given control and target qubits. @@ -286,7 +293,6 @@ def controlled_gate( # The control_value is the location of this unitary. block_matrices = [np.array([[1, 0], [0, 1]])] * 2**num_controls block_matrices[control_value] = U.full() - from scipy.linalg import block_diag # move this to the top of the file result = block_diag(*block_matrices) result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) @@ -295,4 +301,4 @@ def controlled_gate( if controls + targets == list(range(N)): return result else: - return expand_operator(result, N, targets=controls + targets) + return expand_operator(result, targets=controls + targets) From da8da909a939bc16098ffd6c37e72e23707faf61 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 19:23:05 +0530 Subject: [PATCH 006/117] Added namespace to Gates --- src/qutip_qip/operations/gateclass.py | 95 +++++++++++++++++---------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index dd3cefc08..904b97d18 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -8,30 +8,38 @@ class _GateMetaClass(ABCMeta): + _registry = set() + _instances = {} + _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] _read_only_set = set(_read_only) - _instances = {} - - def __setattr__(cls, name: str, value: any) -> None: + def __init__(cls, name, bases, attrs): """ - 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. + 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 a conflict is detected, 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) - For example: - class X(Gate): - num_qubits = 1 # Allowed (during class creation) + # Don't register the Abstract Gate Classes or private helpers + if name.startswith("_") or inspect.isabstract(cls): + return - But: - X.num_qubits = 2 # Raises AttributeError (prevention of overwrite) + namespace = attrs.get("namespace", "std") + key = (namespace, cls.name) - This is required since num_qubits etc. are class attributes (shared by all object instances). - """ - for attribute in cls._read_only_set: - if name == attribute and hasattr(cls, attribute): - raise AttributeError(f"{attribute} is read-only!") - super().__setattr__(name, value) + if key in cls._registry: + raise TypeError( + f"Gate Conflict: '{cls.name}' is already defined in namespace '{namespace}' " + ) + + cls._registry.add(key) def __call__(cls, *args, **kwargs): """ @@ -55,6 +63,26 @@ def __call__(cls, *args, **kwargs): return cls._instances[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). + """ + for attribute in cls._read_only_set: + if name == attribute and hasattr(cls, attribute): + raise AttributeError(f"{attribute} is read-only!") + super().__setattr__(name, value) + class Gate(ABC, metaclass=_GateMetaClass): r""" @@ -91,6 +119,7 @@ class attribute for subclasses. # This helps save memory, faster lookup time & restrict adding new attributes to class. __slots__ = () + _namespace: str = "std" num_qubits: int | None = None self_inverse: bool = False is_clifford: bool = False @@ -123,12 +152,11 @@ def __init_subclass__(cls, **kwargs): # print(H.name) -> 'Hadamard' - if "name" not in cls.__dict__: - # This __dict__ belongs to the class, __slots__ removes __dict__ from object instances + 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 cls.__dict__: + if "latex_str" not in vars(cls): cls.latex_str = cls.__name__ # Assert num_qubits is a non-negative integer @@ -340,18 +368,13 @@ class ControlledGate(Gate): def __init_subclass__(cls, **kwargs): """ Validates the subclass definition. - - Ensures that: - 1. `num_ctrl_qubits` is a positive integer. - 2. `num_ctrl_qubits` is less than the total `num_qubits`. - 3. The sum of `num_ctrl_qubits` and `target.num_qubits` equals the total `num_qubits`. """ super().__init_subclass__(**kwargs) if inspect.isabstract(cls): return - # Assert num_ctrl_qubits is a positive integer + # 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( @@ -359,20 +382,24 @@ def __init_subclass__(cls, **kwargs): 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'") - # Assert num_ctrl_qubits + target_gate.num_qubits = 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}") - # Default value for control_value - cls._control_value = 2**cls.num_ctrl_qubits - 1 - # Automatically copy the validator from the target if hasattr(cls, "target_gate") and hasattr(cls.target_gate, "validate_params"): cls.validate_params = staticmethod(cls.target_gate.validate_params) + # Default value for control_value (can be changed by individual gate instance) + cls._control_value = 2**cls.num_ctrl_qubits - 1 + + # Default set_inverse + # cls.self_inverse = cls.target_gate.self_inverse + # In the circuit plot, only the target gate is shown. # The control has its own symbol. cls.latex_str = cls.target_gate.latex_str @@ -435,14 +462,12 @@ def get_qobj(self) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the controlled operation. """ + target_gate = self.target_gate if self.is_parametric_gate(): - return controlled_gate( - U=self.target_gate(self.arg_value).get_qobj(), - control_value=self.control_value, - ) + target_gate = target_gate(self.arg_value) return controlled_gate( - U=self.target_gate.get_qobj(), + U=target_gate.get_qobj(), control_value=self.control_value, ) From 6ee5ca47c3e2ff2b658c994a4cf7690a245320a5 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 15 Feb 2026 19:43:57 +0530 Subject: [PATCH 007/117] Resolved errors in qpe, vqa based on namespace --- src/qutip_qip/algorithms/qpe.py | 10 ++++-- src/qutip_qip/operations/gateclass.py | 32 +++++++++++++++---- src/qutip_qip/qasm.py | 8 +++++ src/qutip_qip/vqa.py | 4 ++- tests/test_qasm.py | 46 ++++++++++++++------------- tests/test_qpe.py | 6 +--- 6 files changed, 69 insertions(+), 37 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index db6a828c0..00805bf86 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,9 @@ import numpy as np from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence -from qutip_qip.operations import custom_gate_factory, controlled_gate_factory, H +from qutip_qip.operations import ( + custom_gate_factory, controlled_gate_factory, H, Gate +) def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): @@ -54,6 +56,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): for i in range(num_counting_qubits): qc.add_gate(H, targets=[i]) + Gate.clear_cache("qpe") # Apply controlled-U gates with increasing powers for i in range(num_counting_qubits): power = 2 ** (num_counting_qubits - i - 1) @@ -63,9 +66,10 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): # Add controlled-U^power gate controlled_u = controlled_gate_factory( gate=custom_gate_factory( - gate_name="U^power gate", + gate_name=f"U^{power}", + user_namespace = "qpe", U=U_power, - )(), + ), ) qc.add_gate(controlled_u, targets=target_qubits, controls=[i]) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 904b97d18..e9f10dbd4 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -8,7 +8,7 @@ class _GateMetaClass(ABCMeta): - _registry = set() + _registry = {} _instances = {} _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] @@ -32,14 +32,31 @@ def __init__(cls, name, bases, attrs): return namespace = attrs.get("namespace", "std") - key = (namespace, cls.name) + name = cls.name - if key in cls._registry: + if namespace not in cls._registry: + cls._registry[namespace] = set() + + if name in cls._registry[namespace]: raise TypeError( - f"Gate Conflict: '{cls.name}' is already defined in namespace '{namespace}' " + f"Gate Conflict: '{name}' is already defined in namespace '{namespace}' " ) + cls._registry[namespace].add(name) + + def clear_cache(cls, namespace: str): + """ + Clears the gate class registry based on the namespace. - cls._registry.add(key) + Parameters + ---------- + namespace : str, optional + If provided, only clears gates belonging to this namespace + (e.g., 'custom'). If None, clears ALL gates (useful for hard resets). + """ + if namespace == "std": + raise ValueError("Can't clear std Gates") + else: + cls._registry[namespace] = set() def __call__(cls, *args, **kwargs): """ @@ -483,7 +500,7 @@ def __str__(self) -> str: return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" -def custom_gate_factory(gate_name: str, U: Qobj) -> Gate: +def custom_gate_factory(gate_name: str, U: Qobj, user_namespace: str = "custom") -> Gate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ @@ -491,6 +508,7 @@ def custom_gate_factory(gate_name: str, U: Qobj) -> Gate: inverse = (U == U.dag()) class CustomGate(Gate): + namespace = user_namespace latex_str = r"U" name = gate_name num_qubits = int(np.log2(U.shape[0])) @@ -508,6 +526,7 @@ def get_qobj(): def controlled_gate_factory( gate: Gate, + user_namespace: str = "custom", n_ctrl_qubits: int = 1, control_value: int = -1, ) -> ControlledGate: @@ -516,6 +535,7 @@ def controlled_gate_factory( """ class _CustomGate(ControlledGate): + namespace = user_namespace latex_str = rf"C{gate.name}" target_gate = gate num_qubits = n_ctrl_qubits + target_gate.num_qubits diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index fe343cd04..b8dec9c42 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -185,6 +185,7 @@ def __init__(self, commands, mode="default", version="2.0"): "crz", "cu1", "cu3", + "swap", ] ) self.predefined_gates = self.predefined_gates.union( @@ -617,6 +618,13 @@ def _add_qiskit_gates( classical_controls=classical_controls, classical_control_value=classical_control_value, ) + elif name == "swap": + qc.add_gate( + std.SWAP, + controls=regs, + classical_controls=classical_controls, + classical_control_value=classical_control_value, + ) elif name == "ccx": qc.add_gate( std.TOFFOLI, diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index f59436b21..fd09c40af 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -7,7 +7,7 @@ from qutip_qip.circuit import QubitCircuit from scipy.optimize import minimize from scipy.linalg import expm_frechet -from qutip_qip.operations import gate_sequence_product, custom_gate_factory +from qutip_qip.operations import gate_sequence_product, custom_gate_factory, Gate class VQA: @@ -127,6 +127,7 @@ def construct_circuit(self, angles): circ = QubitCircuit(self.num_qubits) i = 0 for layer_num in range(self.num_layers): + Gate.clear_cache("vqa") for block in self.blocks: if block.initial and layer_num > 0: continue @@ -138,6 +139,7 @@ def construct_circuit(self, angles): current_params = angles[i : i + n] if n > 0 else [] gate_instance = custom_gate_factory( gate_name=block.name, + user_namespace = "vqa", U=block.get_unitary(current_params), ) diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 8870cf8ce..c99d753b3 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -7,7 +7,7 @@ from qutip_qip.qasm import read_qasm, circuit_to_qasm_str from qutip_qip.circuit import QubitCircuit from qutip import tensor, rand_ket, basis, identity -from qutip_qip.operations import Measurement +from qutip_qip.operations import Measurement, Gate import qutip_qip.operations.std as std @@ -171,16 +171,17 @@ def test_export_import(): assert (u0 - u1).norm() < 1e-12 -def test_read_qasm(): +def test_read_qasm_1(): + Gate.clear_cache(namespace="custom") filename = "w-state.qasm" filepath = Path(__file__).parent / "qasm_files" / filename + read_qasm(filepath) + +def test_read_qasm_2(): + Gate.clear_cache(namespace="custom") filename2 = "w-state_with_comments.qasm" filepath2 = Path(__file__).parent / "qasm_files" / filename2 - - read_qasm(filepath) read_qasm(filepath2) - assert True - def test_parsing_mode(tmp_path): mode = "qiskit" @@ -195,22 +196,23 @@ def test_parsing_mode(tmp_path): ) assert "Unknown parsing mode" in record_warning[0].message.args[0] - mode = "predefined_only" - qasm_input_string = ( - 'OPENQASM 2.0;\ninclude "qelib1.inc"\n\ncreg c[2];' - "\nqreg q[2];swap q[0],q[1];\n" - ) - with pytest.raises(SyntaxError): - with pytest.warns(UserWarning) as record_warning: - circuit = read_qasm( - qasm_input_string, - mode=mode, - strmode=True, - ) - assert ( - "Ignoring external gate definition in the predefined_only mode." - in record_warning[0].message.args[0] - ) + # TODO fix this test, since SWAP is now a predefined gate + # mode = "predefined_only" + # qasm_input_string = ( + # 'OPENQASM 2.0;\ninclude "qelib1.inc"\n\ncreg c[2];' + # "\nqreg q[2];swap q[0],q[1];\n" + # ) + # with pytest.raises(SyntaxError): + # with pytest.warns(UserWarning) as record_warning: + # circuit = read_qasm( + # qasm_input_string, + # mode=mode, + # strmode=True, + # ) + # assert ( + # "Ignoring external gate definition in the predefined_only mode." + # in record_warning[0].message.args[0] + # ) mode = "external_only" file_path = tmp_path / "custom_swap.inc" diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 41dd21bee..44851fe82 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -2,11 +2,7 @@ from numpy.testing import assert_, assert_equal import unittest from qutip import Qobj, sigmaz, tensor -from qutip_qip.operations import ( - ControlledGate, - controlled_gate_factory, - custom_gate_factory, -) +from qutip_qip.operations import controlled_gate_factory, custom_gate_factory from qutip_qip.algorithms.qpe import qpe From 21f571a1b143b335930afd4e80189d5b3551a146 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Fri, 20 Feb 2026 14:52:15 +0530 Subject: [PATCH 008/117] Made Gate class not instantiable by default --- src/qutip_qip/circuit/circuit.py | 3 +- src/qutip_qip/operations/gateclass.py | 41 ++++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 6745cec8f..6dc6a01c2 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -370,9 +370,8 @@ def add_gate( elif gate_class.is_controlled_gate(): gate = gate_class(control_value=control_value) - else: - gate = gate() + gate = gate_class qubits = [] if controls is not None: diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index e9f10dbd4..241174318 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -9,7 +9,6 @@ class _GateMetaClass(ABCMeta): _registry = {} - _instances = {} _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] _read_only_set = set(_read_only) @@ -60,25 +59,16 @@ def clear_cache(cls, namespace: str): def __call__(cls, *args, **kwargs): """ - This is meant to add Singleton Design for Gates without any args - like arg_value, control_value etc. - - So creating X() 10,000 times (e.g., for a large circuit) becomes instant + So creating CNOT(control_value=1) 10,000 times (e.g., for a large circuit) becomes instant because it's just a dictionary lookup after the first time. Also in memory - we only need to store one copy of X gate, no matter how many times it appears + we only need to store one copy of CNOT gate, no matter how many times it appears in the circuit. """ - + return super().__call__(*args, **kwargs) + # For RX(0.5), RX(0.1) we want different instances. # Same for CX(control_value=0), CX(control_value=1) - if args or kwargs: - return super().__call__(*args, **kwargs) - - # For non-parametric gates (like X, H, Z), only one instance - if cls not in cls._instances: - cls._instances[cls] = super().__call__(*args, **kwargs) - - return cls._instances[cls] + # TODO This needs to be implemented efficiently def __setattr__(cls, name: str, value: any) -> None: """ @@ -184,6 +174,16 @@ def __init_subclass__(cls, **kwargs): f"got {type(num_qubits)} with value {num_qubits}." ) + def __init__(self) -> None: + """ + This method is overwritten by Parametrized and Controlled Gates. + """ + raise TypeError( + f"Gate '{type(self).__name__}' does not accept initialization arguments. " + f"If your gate requires parameters, it must inherit from 'ParametricGate'." + f"Or if it must be controlled, it must inherit from 'ControlledGate'." + ) + @staticmethod @abstractmethod def get_qobj() -> Qobj: @@ -508,8 +508,8 @@ def custom_gate_factory(gate_name: str, U: Qobj, user_namespace: str = "custom") inverse = (U == U.dag()) class CustomGate(Gate): + __slots__ = () namespace = user_namespace - latex_str = r"U" name = gate_name num_qubits = int(np.log2(U.shape[0])) self_inverse = inverse @@ -526,20 +526,21 @@ def get_qobj(): def controlled_gate_factory( gate: Gate, - user_namespace: str = "custom", n_ctrl_qubits: int = 1, control_value: int = -1, + user_namespace: str = "custom", ) -> ControlledGate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ class _CustomGate(ControlledGate): + __slots__ = () namespace = user_namespace - latex_str = rf"C{gate.name}" - target_gate = gate - num_qubits = n_ctrl_qubits + target_gate.num_qubits + num_qubits = n_ctrl_qubits + gate.num_qubits num_ctrl_qubits = n_ctrl_qubits + target_gate = gate + latex_str = rf"C{gate.name}" @property def control_value(self) -> int: From 4e52dd6c0ec99990ef4014d1fe659a2610d892d4 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Fri, 20 Feb 2026 15:39:51 +0530 Subject: [PATCH 009/117] Add an additional test for qiskit backend --- tests/test_qiskit.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index d7dd00eb8..78b3732b1 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -15,7 +15,7 @@ CircularSpinChain, DispersiveCavityQED, ) -from qutip_qip.operations import X, CX, RX +from qutip_qip.operations import X, CX, RX, H from qiskit import QuantumCircuit from qiskit_aer import AerSimulator @@ -134,6 +134,24 @@ def test_controlled_qubit_conversion(self): assert self._compare_circuit(result_circuit, required_circuit) def test_rotation_conversion(self): + """ + Test to check conversion of a circuit + containing a single qubit rotation gate. + """ + qiskit_circuit = QuantumCircuit(3) + qiskit_circuit.rx(np.pi / 3, 0) + qiskit_circuit.cx(0, 1) + qiskit_circuit.h(2) + result_circuit = convert_qiskit_circuit_to_qutip(qiskit_circuit) + + required_circuit = QubitCircuit(3) + required_circuit.add_gate(RX(np.pi / 3), targets=[0]) + required_circuit.add_gate(CX, targets=[1], controls=[0]) + required_circuit.add_gate(H, targets=[2]) + + assert self._compare_circuit(result_circuit, required_circuit) + + def test_multiqubit_circuit_conversion(self): """ Test to check conversion of a circuit containing a single qubit rotation gate. From ab79f342cbafa0a10ec4344fa9ad0f959b1d9b57 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 21 Feb 2026 12:09:27 +0530 Subject: [PATCH 010/117] Added gate equivalence checking --- src/qutip_qip/compiler/instruction.py | 1 - src/qutip_qip/operations/gateclass.py | 165 +++++++++++++++++--------- src/qutip_qip/operations/gates.py | 109 +++++------------ src/qutip_qip/operations/utils.py | 8 +- 4 files changed, 143 insertions(+), 140 deletions(-) diff --git a/src/qutip_qip/compiler/instruction.py b/src/qutip_qip/compiler/instruction.py index b01a0b414..a04871846 100644 --- a/src/qutip_qip/compiler/instruction.py +++ b/src/qutip_qip/compiler/instruction.py @@ -1,6 +1,5 @@ from copy import deepcopy import numpy as np -from qutip_qip.operations import ControlledGate class PulseInstruction: diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 241174318..33b067feb 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -1,4 +1,6 @@ -from __future__ import annotations # This won't be needed after minimum version becomes 3.14 (PEP 749) +from __future__ import ( + annotations, +) # This won't be needed after minimum version becomes 3.14 (PEP 749) from abc import ABC, ABCMeta, abstractmethod import inspect @@ -8,17 +10,24 @@ class _GateMetaClass(ABCMeta): - _registry = {} - - _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] - _read_only_set = set(_read_only) + _registry: dict[str, set] = {} + _read_only_set: set[str] = set( + ( + "num_qubits", + "num_ctrl_qubits", + "num_params", + "target_gate", + "self_inverse", + "is_clifford", + ) + ) 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 a conflict is detected, it raises a strict TypeError to prevent - ambiguous gate definitions. + 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 @@ -49,7 +58,7 @@ def clear_cache(cls, namespace: str): Parameters ---------- namespace : str, optional - If provided, only clears gates belonging to this namespace + If provided, only clears gates belonging to this namespace (e.g., 'custom'). If None, clears ALL gates (useful for hard resets). """ if namespace == "std": @@ -59,9 +68,9 @@ def clear_cache(cls, namespace: str): def __call__(cls, *args, **kwargs): """ - So creating CNOT(control_value=1) 10,000 times (e.g., for a large circuit) becomes instant + So creating CNOT(control_value=1) 10,000 times (e.g., for a large circuit) becomes instant because it's just a dictionary lookup after the first time. Also in memory - we only need to store one copy of CNOT gate, no matter how many times it appears + we only need to store one copy of CNOT gate, no matter how many times it appears in the circuit. """ return super().__call__(*args, **kwargs) @@ -72,8 +81,8 @@ def __call__(cls, *args, **kwargs): 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 + 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: @@ -95,18 +104,18 @@ class Gate(ABC, metaclass=_GateMetaClass): r""" Abstract base class for a quantum gate. - Concrete gate classes or gate implementations should be defined as subclasses + 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 + 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 + The number of qubits the gate acts upon. This is a mandatory class attribute for subclasses. self_inverse: bool @@ -114,13 +123,14 @@ class attribute for subclasses. Default value is False. is_clifford: bool - Indicates if the gate belongs to the Clifford group, which maps + 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. @@ -138,32 +148,34 @@ def __init_subclass__(cls, **kwargs): """ 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 + 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. """ super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): # Skip the below check for an abstract class + if inspect.isabstract( + cls + ): # Skip the below check for an abstract class return # 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): + if "latex_str" not in vars(cls): cls.latex_str = cls.__name__ # Assert num_qubits is a non-negative integer @@ -179,9 +191,9 @@ def __init__(self) -> None: This method is overwritten by Parametrized and Controlled Gates. """ raise TypeError( - f"Gate '{type(self).__name__}' does not accept initialization arguments. " - f"If your gate requires parameters, it must inherit from 'ParametricGate'." - f"Or if it must be controlled, it must inherit from 'ControlledGate'." + f"Gate '{type(self).name}' can't be initialized. " + f"If your gate requires parameters, it must inherit from 'ParametricGate'. " + f"Or if it must be controlled and needs control_value, it must inherit from 'ControlledGate'." ) @staticmethod @@ -197,11 +209,12 @@ def get_qobj() -> Qobj: """ pass - def inverse(self) -> Gate: + @classmethod + def inverse(cls) -> Gate: """ Return the inverse of the gate. - If ``self_inverse`` is True, returns ``self``. Otherwise, + If ``self_inverse`` is True, returns ``self``. Otherwise, returns the specific inverse gate class. Returns @@ -209,9 +222,9 @@ def inverse(self) -> Gate: Gate A Gate instance representing $G^{-1}$. """ - if self.self_inverse: - return self - # Implement this via gate factory? + if cls.self_inverse: + return cls + raise NotImplementedError @staticmethod def is_controlled_gate() -> bool: @@ -251,20 +264,20 @@ class ParametricGate(Gate): ---------- arg_value : float or Sequence The argument value(s) for the gate. If a single float is provided, - it is converted to a list. These values are saved as attributes + it is converted to a list. These values are saved as attributes and can be accessed or modified later. arg_label : str, optional - Label for the argument to be shown in the circuit plot. - + Label for the argument to be shown in the circuit plot. + Example: - If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit + If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit plot will be rendered as ``$U(\phi)$``. Attributes ---------- num_params : int - The number of parameters required by the gate. This is a mandatory + The number of parameters required by the gate. This is a mandatory class attribute for subclasses. arg_value : Sequence @@ -278,7 +291,8 @@ class attribute for subclasses. ValueError If the number of provided arguments does not match `num_params`. """ - __slots__ = ('arg_value', 'arg_label') + + __slots__ = ("arg_value", "arg_label") num_params: int | None = None @@ -305,7 +319,9 @@ def __init__(self, arg_value: float, arg_label: str | None = None): arg_value = [arg_value] if len(arg_value) != self.num_params: - raise ValueError(f"Requires {self.num_params} parameters, got {len(arg_value)} parameters") + raise ValueError( + f"Requires {self.num_params} parameters, got {len(arg_value)} parameters" + ) self.validate_params(arg_value) self.arg_value = arg_value @@ -317,7 +333,7 @@ def validate_params(arg_value): r""" Validate the provided parameters. - This method should be implemented by subclasses to check if the + This method should be implemented by subclasses to check if the parameters are valid type and within valid range (e.g., $0 \le \theta < 2\pi$). Parameters @@ -349,26 +365,34 @@ def __str__(self): arg_label={self.arg_label}), """ + def __eq__(self, other) -> bool: + if type(self) is not type(other): + return False + + if self.arg_value != other.arg_value: + return False + return True + 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. + A controlled gate applies a target unitary operation only when the control + qubits are in a specific state. Parameters ---------- control_value : int, optional - The decimal value of the control state required to execute the + The decimal value of the control state required to execute the unitary operator on the target qubits. - + Examples: - * If the gate should execute when the 0-th qubit is $|1\rangle$, + * If the gate should execute when the 0-th qubit is $|1\rangle$, set ``control_value=1``. - * If the gate should execute when two control qubits are $|10\rangle$ + * If the gate should execute when two control qubits are $|10\rangle$ (binary 10), set ``control_value=0b10``. - + Defaults to all-ones (e.g., $2^N - 1$) if not provided. Attributes @@ -379,7 +403,8 @@ class ControlledGate(Gate): target_gate : Gate The gate to be applied to the target qubits. """ - __slots__ = ('arg_value', 'arg_label') + + __slots__ = ("arg_value", "arg_label") num_ctrl_qubits: int | None = None def __init_subclass__(cls, **kwargs): @@ -401,14 +426,20 @@ def __init_subclass__(cls, **kwargs): # 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'") + 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}") + 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}" + ) # Automatically copy the validator from the target - if hasattr(cls, "target_gate") and hasattr(cls.target_gate, "validate_params"): + if hasattr(cls, "target_gate") and hasattr( + cls.target_gate, "validate_params" + ): cls.validate_params = staticmethod(cls.target_gate.validate_params) # Default value for control_value (can be changed by individual gate instance) @@ -432,7 +463,9 @@ def __init__( self._control_value = control_value if self.is_parametric_gate(): - ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) + ParametricGate.__init__( + self, arg_value=arg_value, arg_label=arg_label + ) @property @abstractmethod @@ -457,18 +490,24 @@ def _validate_control_value(cls, control_value: int) -> None: TypeError If control_value is not an integer. ValueError - If control_value is negative or exceeds the maximum value + If control_value is negative or exceeds the maximum value possible for the number of control qubits ($2^N - 1$). """ if type(control_value) is not int: - raise TypeError(f"Control value must be an int, got {control_value}") + raise TypeError( + f"Control value must be an int, got {control_value}" + ) if control_value < 0: - raise ValueError(f"Control value can't be negative, got {control_value}") + raise ValueError( + f"Control value can't be negative, got {control_value}" + ) if control_value > 2**cls.num_ctrl_qubits - 1: - raise ValueError(f"Control value can't be greater than 2^num_ctrl_qubits - 1, got {control_value}") + raise ValueError( + f"Control value can't be greater than 2^num_ctrl_qubits - 1, got {control_value}" + ) def get_qobj(self) -> Qobj: """ @@ -499,13 +538,23 @@ def is_parametric_gate(cls) -> bool: def __str__(self) -> str: return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" + def __eq__(self, other) -> bool: + if type(self) is not type(other): + return False + + if self.control_value != other.control_value: + return False + return True + -def custom_gate_factory(gate_name: str, U: Qobj, user_namespace: str = "custom") -> Gate: +def custom_gate_factory( + gate_name: str, U: Qobj, user_namespace: str = "custom" +) -> Gate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ - inverse = (U == U.dag()) + inverse = U == U.dag() class CustomGate(Gate): __slots__ = () diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/gates.py index 1d57acf27..87b8f6db5 100644 --- a/src/qutip_qip/operations/gates.py +++ b/src/qutip_qip/operations/gates.py @@ -13,6 +13,7 @@ from qutip import Qobj, identity, qeye, sigmax, sigmay, sigmaz, tensor, fock_dm from qutip_qip.operations import expand_operator + # Single Qubit Gates def _deprecation_warnings_gate_expansion(): warnings.warn( @@ -37,9 +38,7 @@ def x_gate(N=None, target=0): """ warnings.warn( "x_gate has been deprecated and will be removed in future version. \ - Use X.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use X.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -59,9 +58,7 @@ def y_gate(N=None, target=0): """ warnings.warn( "Y_gate has been deprecated and will be removed in future version. \ - Use Y.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use Y.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -81,9 +78,7 @@ def z_gate(N=None, target=0): """ warnings.warn( "z_gate has been deprecated and will be removed in future version. \ - Use Z.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use Z.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -102,9 +97,7 @@ def cy_gate(N=None, control=0, target=1): """ warnings.warn( "cy_gate has been deprecated and will be removed in future version. \ - Use CY.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CY.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -131,9 +124,7 @@ def cz_gate(N=None, control=0, target=1): """ warnings.warn( "cz_gate has been deprecated and will be removed in future version. \ - Use CZ.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CZ.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -161,9 +152,7 @@ def s_gate(N=None, target=0): """ warnings.warn( "s_gate has been deprecated and will be removed in future version. \ - Use S.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use S.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -182,9 +171,7 @@ def cs_gate(N=None, control=0, target=1): """ warnings.warn( "cs_gate has been deprecated and will be removed in future version. \ - Use CS.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CS.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -211,9 +198,7 @@ def t_gate(N=None, target=0): """ warnings.warn( "t_gate has been deprecated and will be removed in future version. \ - Use T.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use T.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -232,9 +217,7 @@ def ct_gate(N=None, control=0, target=1): """ warnings.warn( "ct_gate has been deprecated and will be removed in future version. \ - Use CT.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CT.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -266,9 +249,7 @@ def rx(phi, N=None, target=0): """ warnings.warn( "rxRTNOT has been deprecated and will be removed in future version. \ - Use RX(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RX(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -292,9 +273,7 @@ def ry(phi, N=None, target=0): """ warnings.warn( "ryRTNOT has been deprecated and will be removed in future version. \ - Use RY(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RY(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -318,9 +297,7 @@ def rz(phi, N=None, target=0): """ warnings.warn( "rzRTNOT has been deprecated and will be removed in future version. \ - Use RZ(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RZ(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -339,9 +316,7 @@ def sqrtnot(N=None, target=0): """ warnings.warn( "sqrtnot has been deprecated and will be removed in future version. \ - Use SQRTNOT.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTNOT.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -369,9 +344,7 @@ def snot(N=None, target=0): """ warnings.warn( "snot has been deprecated and will be removed in future version. \ - Use H.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use H.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -405,9 +378,7 @@ def phasegate(theta, N=None, target=0): """ warnings.warn( "phase has been deprecated and will be removed in future version. \ - Use PHASE(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use PHASE(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -440,7 +411,7 @@ def qrot(theta, phi, N=None, target=0): "qrot has been deprecated and will be removed in future version. \ Use R([theta, phi]).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if N is not None: _deprecation_warnings_gate_expansion() @@ -487,7 +458,7 @@ def qasmu_gate(args, N=None, target=0): "qasmu_gate has been deprecated and will be removed in future version. \ Use QASMU([theta, phi, gamma]).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) theta, phi, gamma = args @@ -531,7 +502,7 @@ def cphase(theta, N=2, control=0, target=1): "cphase has been deprecated and will be removed in future version. \ Use CPHASE(angle).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if N != 2 or control != 0 or target != 1: @@ -578,9 +549,7 @@ def cnot(N=None, control=0, target=1): """ warnings.warn( "cnot has been deprecated and will be removed in future version. \ - Use CX.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CX.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: @@ -618,9 +587,7 @@ def csign(N=None, control=0, target=1): """ warnings.warn( "csign has been deprecated and will be removed in future version. \ - Use CZ.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CZ.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: @@ -658,9 +625,7 @@ def berkeley(N=None, targets=[0, 1]): """ warnings.warn( "berkley has been deprecated and will be removed in future version. \ - Use BERKELEY.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use BERKELEY.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (targets[0] == 1 and targets[1] == 0) and N is None: @@ -705,7 +670,7 @@ def swapalpha(alpha, N=None, targets=[0, 1]): "swapalpha has been deprecated and will be removed in future version. \ Use SWAPALPHA(angle).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if (targets[0] == 1 and targets[1] == 0) and N is None: @@ -757,9 +722,7 @@ def swap(N=None, targets=[0, 1]): """ warnings.warn( "SWAP has been deprecated and will be removed in future version. \ - Use SWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -794,9 +757,7 @@ def iswap(N=None, targets=[0, 1]): """ warnings.warn( "ISWAP has been deprecated and will be removed in future version. \ - Use ISWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use ISWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -821,9 +782,7 @@ def sqrtswap(N=None, targets=[0, 1]): """ warnings.warn( "SQRTSWAP has been deprecated and will be removed in future version. \ - Use SQRTSWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTSWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -870,9 +829,7 @@ def sqrtiswap(N=None, targets=[0, 1]): """ warnings.warn( "SQRTISWAP has been deprecated and will be removed in future version. \ - Use SQRTISWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTISWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -917,7 +874,7 @@ def molmer_sorensen(theta, phi=0.0, N=None, targets=[0, 1]): "MS has been deprecated and will be removed in future version. \ Use MS([theta, phi]).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if targets != [0, 1] and N is None: N = 2 @@ -979,9 +936,7 @@ def fredkin(N=None, control=0, targets=[1, 2]): """ warnings.warn( "fredkin has been deprecated and will be removed in future version. \ - Use FREDKIN.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use FREDKIN.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if [control, targets[0], targets[1]] != [0, 1, 2] and N is None: N = 3 @@ -1032,9 +987,7 @@ def toffoli(N=None, controls=[0, 1], target=2): """ warnings.warn( "toffoli has been deprecated and will be removed in future version. \ - Use TOFFOLI.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use TOFFOLI.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if [controls[0], controls[1], target] != [0, 1, 2] and N is None: N = 3 @@ -1107,7 +1060,7 @@ def globalphase(theta, N=1): "global_phase has been deprecated and will be removed in future version. \ Use GLOBALPHASE(phase).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) data = np.exp(1.0j * theta) * sp.eye( 2**N, 2**N, dtype=complex, format="csr" diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index de96c2e6a..9f3dcff74 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -8,6 +8,7 @@ import qutip from qutip import Qobj, identity, tensor + def _check_oper_dims(oper: Qobj, dims=None, targets=None): """ Check if the given operator is valid. @@ -77,13 +78,14 @@ def _targets_to_list(targets, oper=None, N=None): raise ValueError("Targets must be smaller than N={}.".format(N)) return targets + def expand_operator( oper: Qobj, N: None = None, targets: int | list[int] | None = None, dims: list[int] | None = None, cyclic_permutation: bool = False, - dtype: str | None = None + dtype: str | None = None, ): """ Expand an operator to one that acts on a system with desired dimensions. @@ -163,7 +165,7 @@ def expand_operator( "Please use the new signature e.g.\n" "expand_operator(oper, dims=[2, 3, 2, 2], targets=2)", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if dims is not None and N is None: @@ -182,7 +184,7 @@ def expand_operator( "cyclic_permutation is deprecated, " "please use loop through different targets manually.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) oper_list = [] for i in range(N): From 1d64f8fa41291e106dfe32692939c6b92c601800 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 22 Feb 2026 02:46:10 +0530 Subject: [PATCH 011/117] Renamed custom_gate, controlled_gate factory functions --- src/qutip_qip/algorithms/qpe.py | 6 ++-- src/qutip_qip/operations/__init__.py | 8 ++--- src/qutip_qip/operations/gateclass.py | 52 ++++++++++++--------------- src/qutip_qip/qasm.py | 4 +-- src/qutip_qip/vqa.py | 4 +-- tests/test_qpe.py | 10 +++--- 6 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 00805bf86..9e6b0030c 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -2,7 +2,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.operations import ( - custom_gate_factory, controlled_gate_factory, H, Gate + unitary_gate, controlled, H, Gate ) @@ -64,8 +64,8 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): U_power = U if power == 1 else U**power # Add controlled-U^power gate - controlled_u = controlled_gate_factory( - gate=custom_gate_factory( + controlled_u = controlled( + gate=unitary_gate( gate_name=f"U^{power}", user_namespace = "qpe", U=U_power, diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index b5e39304b..680e99aee 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -47,8 +47,8 @@ Gate, ControlledGate, ParametricGate, - custom_gate_factory, - controlled_gate_factory, + unitary_gate, + controlled, AngleParametricGate, ) from .std import ( @@ -143,8 +143,8 @@ "Gate", "ParametricGate", "ControlledGate", - "custom_gate_factory", - "controlled_gate_factory", + "unitary_gate", + "controlled", "AngleParametricGate", "Measurement", "GATE_CLASS_MAP", diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 33b067feb..fc477232a 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -404,7 +404,7 @@ class ControlledGate(Gate): The gate to be applied to the target qubits. """ - __slots__ = ("arg_value", "arg_label") + __slots__ = ("arg_value", "arg_label", "_control_value") num_ctrl_qubits: int | None = None def __init_subclass__(cls, **kwargs): @@ -442,9 +442,6 @@ def __init_subclass__(cls, **kwargs): ): cls.validate_params = staticmethod(cls.target_gate.validate_params) - # Default value for control_value (can be changed by individual gate instance) - cls._control_value = 2**cls.num_ctrl_qubits - 1 - # Default set_inverse # cls.self_inverse = cls.target_gate.self_inverse @@ -461,6 +458,8 @@ def __init__( if control_value is not None: self._validate_control_value(control_value) self._control_value = control_value + else: + self._control_value = (2 ** self.num_ctrl_qubits) - 1 if self.is_parametric_gate(): ParametricGate.__init__( @@ -499,14 +498,10 @@ def _validate_control_value(cls, control_value: int) -> None: f"Control value must be an int, got {control_value}" ) - if control_value < 0: + if control_value < 0 or control_value > 2**cls.num_ctrl_qubits - 1: raise ValueError( - f"Control value can't be negative, got {control_value}" - ) - - if control_value > 2**cls.num_ctrl_qubits - 1: - raise ValueError( - f"Control value can't be greater than 2^num_ctrl_qubits - 1, got {control_value}" + f"Control value can't be negative and can't be greater than " + f"2^num_ctrl_qubits - 1, got {control_value}" ) def get_qobj(self) -> Qobj: @@ -547,43 +542,46 @@ def __eq__(self, other) -> bool: return True -def custom_gate_factory( +def unitary_gate( gate_name: str, U: Qobj, user_namespace: str = "custom" ) -> Gate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ - inverse = U == U.dag() + n = np.log2(U.shape[0]) + inverse = (U == U.dag()) + + if n != np.log2(U.shape[1]): + raise ValueError("The unitary U must be square.") - class CustomGate(Gate): + if n % 1 != 0: + raise ValueError("The unitary U must have dim NxN, where N=2^n") + + class _CustomGate(Gate): __slots__ = () - namespace = user_namespace name = gate_name - num_qubits = int(np.log2(U.shape[0])) + namespace = user_namespace + num_qubits = int(n) self_inverse = inverse - def __init__(self): - self._U = U - @staticmethod def get_qobj(): return U - return CustomGate + return _CustomGate -def controlled_gate_factory( +def controlled( gate: Gate, n_ctrl_qubits: int = 1, - control_value: int = -1, user_namespace: str = "custom", ) -> ControlledGate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ - class _CustomGate(ControlledGate): + class _CustomControlledGate(ControlledGate): __slots__ = () namespace = user_namespace num_qubits = n_ctrl_qubits + gate.num_qubits @@ -591,13 +589,7 @@ class _CustomGate(ControlledGate): target_gate = gate latex_str = rf"C{gate.name}" - @property - def control_value(self) -> int: - if control_value == -1: - return 2**n_ctrl_qubits - 1 - return control_value - - return _CustomGate + return _CustomControlledGate class AngleParametricGate(ParametricGate): diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index b8dec9c42..35e2b0401 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -10,7 +10,7 @@ from math import pi # Don't remove from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import custom_gate_factory +from qutip_qip.operations import unitary_gate import qutip_qip.operations.std as std __all__ = ["read_qasm", "save_qasm", "print_qasm", "circuit_to_qasm_str"] @@ -818,7 +818,7 @@ def _gate_add( if custom_gate_unitary is not None: # Instantiate the wrapper gate - gate_obj = custom_gate_factory( + gate_obj = unitary_gate( gate_name=gate_name, U=custom_gate_unitary, ) diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index fd09c40af..d3067c22f 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -7,7 +7,7 @@ from qutip_qip.circuit import QubitCircuit from scipy.optimize import minimize from scipy.linalg import expm_frechet -from qutip_qip.operations import gate_sequence_product, custom_gate_factory, Gate +from qutip_qip.operations import gate_sequence_product, unitary_gate, Gate class VQA: @@ -137,7 +137,7 @@ def construct_circuit(self, angles): n = block.get_free_parameters_num() current_params = angles[i : i + n] if n > 0 else [] - gate_instance = custom_gate_factory( + gate_instance = unitary_gate( gate_name=block.name, user_namespace = "vqa", U=block.get_unitary(current_params), diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 44851fe82..6e2f61763 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -2,7 +2,7 @@ from numpy.testing import assert_, assert_equal import unittest from qutip import Qobj, sigmaz, tensor -from qutip_qip.operations import controlled_gate_factory, custom_gate_factory +from qutip_qip.operations import controlled, unitary_gate from qutip_qip.algorithms.qpe import qpe @@ -14,11 +14,11 @@ class TestQPE(unittest.TestCase): def test_custom_gate(self): """ - Test if custom_gate_factory correctly stores and returns the quantum object + Test if unitary_gate correctly stores and returns the quantum object """ U = Qobj([[0, 1], [1, 0]]) - custom = custom_gate_factory(gate_name="custom", U=U) + custom = unitary_gate(gate_name="custom", U=U) qobj = custom.get_qobj() assert_((qobj - U).norm() < 1e-12) @@ -28,8 +28,8 @@ def test_controlled_unitary(self): """ U = Qobj([[0, 1], [1, 0]]) - controlled_u = controlled_gate_factory( - gate=custom_gate_factory(gate_name="CU", U=U), + controlled_u = controlled( + gate=unitary_gate(gate_name="CU", U=U), )(control_value=1) assert_equal(controlled_u.control_value, 1) From e074634bad7645341fcb58e4d8686ea67ca0296b Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 22 Feb 2026 03:00:24 +0530 Subject: [PATCH 012/117] separated controlled and parametric gate into their own files --- src/qutip_qip/algorithms/qpe.py | 2 +- src/qutip_qip/operations/__init__.py | 13 +- src/qutip_qip/operations/controlled.py | 202 +++++++++++++++ src/qutip_qip/operations/gateclass.py | 340 +------------------------ src/qutip_qip/operations/parametric.py | 135 ++++++++++ src/qutip_qip/vqa.py | 2 +- tests/test_renderer.py | 22 +- 7 files changed, 356 insertions(+), 360 deletions(-) create mode 100644 src/qutip_qip/operations/controlled.py create mode 100644 src/qutip_qip/operations/parametric.py diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 9e6b0030c..35e5c012f 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -67,7 +67,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): controlled_u = controlled( gate=unitary_gate( gate_name=f"U^{power}", - user_namespace = "qpe", + namespace = "qpe", U=U_power, ), ) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 680e99aee..cb13cd330 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -7,6 +7,10 @@ gate_sequence_product, controlled_gate, ) +from .gateclass import Gate, unitary_gate +from .parametric import ParametricGate, AngleParametricGate +from .controlled import ControlledGate, controlled +from .measurement import Measurement from .gates import ( rx, ry, @@ -43,14 +47,6 @@ qubit_clifford_group, ) -from .gateclass import ( - Gate, - ControlledGate, - ParametricGate, - unitary_gate, - controlled, - AngleParametricGate, -) from .std import ( X, Y, @@ -93,7 +89,6 @@ GLOBALPHASE, IDLE, ) -from .measurement import Measurement GATE_CLASS_MAP: dict[str, Gate] = { "GLOBALPHASE": GLOBALPHASE, diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py new file mode 100644 index 000000000..24a240869 --- /dev/null +++ b/src/qutip_qip/operations/controlled.py @@ -0,0 +1,202 @@ +import inspect +from abc import abstractmethod + +from qutip import Qobj +from qutip_qip.operations import Gate, ParametricGate, controlled_gate + + +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. + + Parameters + ---------- + control_value : int, optional + The decimal value of the control state required to execute the + unitary operator on the target qubits. + + Examples: + * If the gate should execute when the 0-th qubit is $|1\rangle$, + set ``control_value=1``. + * If the gate should execute when two control qubits are $|10\rangle$ + (binary 10), set ``control_value=0b10``. + + Defaults to all-ones (e.g., $2^N - 1$) if not provided. + + Attributes + ---------- + num_ctrl_qubits : int + The number of qubits acting as controls. + + target_gate : Gate + The gate to be applied to the target qubits. + """ + + __slots__ = ("arg_value", "arg_label", "_control_value") + num_ctrl_qubits: int + target_gate: Gate + + def __init_subclass__(cls, **kwargs): + """ + 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 postive 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}" + ) + + # Automatically copy the validator from the target + if hasattr(cls.target_gate, "validate_params"): + cls.validate_params = staticmethod(cls.target_gate.validate_params) + + # Default set_inverse + # cls.self_inverse = cls.target_gate.self_inverse + + # In the circuit plot, only the target gate is shown. + # The control has its own symbol. + cls.latex_str = cls.target_gate.latex_str + + def __init__( + self, + arg_value: any = None, + control_value: int | None = None, + arg_label: str | None = None, + ) -> None: + if control_value is not None: + self._validate_control_value(control_value) + self._control_value = control_value + else: + self._control_value = (2 ** self.num_ctrl_qubits) - 1 + + if self.is_parametric_gate(): + ParametricGate.__init__( + self, arg_value=arg_value, arg_label=arg_label + ) + + @property + @abstractmethod + def target_gate() -> Gate: + pass + + @property + def self_inverse(self) -> int: + return self.target_gate.self_inverse + + @property + def control_value(self) -> int: + return self._control_value + + @classmethod + def _validate_control_value(cls, control_value: int) -> None: + """ + Internal validation for the control value. + + Raises + ------ + TypeError + If control_value is not an integer. + ValueError + If control_value is negative or exceeds the maximum value + possible for the number of control qubits ($2^N - 1$). + """ + + if type(control_value) is not int: + raise TypeError( + f"Control value must be an int, got {control_value}" + ) + + if control_value < 0 or control_value > 2**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 {control_value}" + ) + + def get_qobj(self) -> Qobj: + """ + Construct the full Qobj representation of the controlled gate. + + Returns + ------- + qobj : qutip.Qobj + The unitary matrix representing the controlled operation. + """ + target_gate = self.target_gate + if self.is_parametric_gate(): + target_gate = target_gate(self.arg_value) + + return controlled_gate( + U=target_gate.get_qobj(), + control_value=self.control_value, + ) + + @staticmethod + def is_controlled_gate() -> bool: + return True + + @classmethod + def is_parametric_gate(cls) -> bool: + return cls.target_gate.is_parametric_gate() + + def __str__(self) -> str: + return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" + + def __eq__(self, other) -> bool: + if type(self) is not type(other): + return False + + if self.control_value != other.control_value: + return False + return True + + +def controlled( + gate: Gate, + n_ctrl_qubits: int = 1, + gate_name: str = "_CustomControlledGate", + namespace: str = "custom", +) -> ControlledGate: + """ + Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. + """ + + class _CustomControlledGate(ControlledGate): + __slots__ = () + _namespace = namespace + name = gate_name + num_qubits = n_ctrl_qubits + gate.num_qubits + num_ctrl_qubits = n_ctrl_qubits + target_gate = gate + latex_str = rf"C{gate.name}" + + return _CustomControlledGate diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index fc477232a..a352ffa8f 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -1,12 +1,10 @@ -from __future__ import ( - annotations, -) # This won't be needed after minimum version becomes 3.14 (PEP 749) +# annotations import won't be needed after minimum version becomes 3.14 (PEP 749) +from __future__ import annotations from abc import ABC, ABCMeta, abstractmethod import inspect import numpy as np from qutip import Qobj -from qutip_qip.operations import controlled_gate class _GateMetaClass(ABCMeta): @@ -36,7 +34,7 @@ def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) # Don't register the Abstract Gate Classes or private helpers - if name.startswith("_") or inspect.isabstract(cls): + if inspect.isabstract(cls) or name.startswith("_"): return namespace = attrs.get("namespace", "std") @@ -137,7 +135,7 @@ class attribute for subclasses. __slots__ = () _namespace: str = "std" - num_qubits: int | None = None + num_qubits: int self_inverse: bool = False is_clifford: bool = False @@ -154,9 +152,9 @@ def __init_subclass__(cls, **kwargs): """ super().__init_subclass__(**kwargs) - if inspect.isabstract( - cls - ): # Skip the below check for an abstract class + + # Skip the below check for an abstract class + if inspect.isabstract(cls): return # If name attribute in subclass is not defined, set it to the name of the subclass @@ -256,295 +254,7 @@ def __repr__(self) -> str: return f"Gate({self.name}, num_qubits={self.num_qubits}, qobj={self.get_qobj()})" -class ParametricGate(Gate): - r""" - Abstract base class for parametric quantum gates. - - Parameters - ---------- - arg_value : float or Sequence - The argument value(s) for the gate. If a single float is provided, - it is converted to a list. These values are saved as attributes - and can be accessed or modified later. - - arg_label : str, optional - Label for the argument to be shown in the circuit plot. - - Example: - If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit - plot will be rendered as ``$U(\phi)$``. - - Attributes - ---------- - num_params : int - The number of parameters required by the gate. This is a mandatory - class attribute for subclasses. - - arg_value : Sequence - The numerical values of the parameters provided to the gate. - - arg_label : str, optional - The LaTeX string representing the parameter variable in circuit plots. - - Raises - ------ - ValueError - If the number of provided arguments does not match `num_params`. - """ - - __slots__ = ("arg_value", "arg_label") - - num_params: int | None = None - - def __init_subclass__(cls, **kwargs) -> None: - """ - Validates the subclass definition. - - Ensures that `num_params` is defined as a positive integer. - """ - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): - return - - # Assert num_params is a positive integer - num_params = getattr(cls, "num_params", None) - if (type(num_params) is not int) or (num_params < 1): - raise TypeError( - f"Class '{cls.__name__}' attribute 'num_params' must be a postive integer, " - f"got {type(num_params)} with value {num_params}." - ) - - def __init__(self, arg_value: float, arg_label: str | None = None): - if type(arg_value) is float or type(arg_value) is np.float64: - arg_value = [arg_value] - - if len(arg_value) != self.num_params: - raise ValueError( - f"Requires {self.num_params} parameters, got {len(arg_value)} parameters" - ) - - self.validate_params(arg_value) - self.arg_value = arg_value - self.arg_label = arg_label - - @staticmethod - @abstractmethod - def validate_params(arg_value): - r""" - Validate the provided parameters. - - This method should be implemented by subclasses to check if the - parameters are valid type and within valid range (e.g., $0 \le \theta < 2\pi$). - - Parameters - ---------- - arg_value : list of float - The parameters to validate. - """ - pass - - @abstractmethod - def get_qobj(self) -> Qobj: - """ - Get the QuTiP quantum object representation using the current parameters. - - Returns - ------- - qobj : qutip.Qobj - The unitary matrix representing the gate with the specific `arg_value`. - """ - pass - - @staticmethod - def is_parametric_gate(): - return True - - def __str__(self): - return f""" - Gate({self.name}, arg_value={self.arg_value}, - arg_label={self.arg_label}), - """ - - def __eq__(self, other) -> bool: - if type(self) is not type(other): - return False - - if self.arg_value != other.arg_value: - return False - return True - - -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. - - Parameters - ---------- - control_value : int, optional - The decimal value of the control state required to execute the - unitary operator on the target qubits. - - Examples: - * If the gate should execute when the 0-th qubit is $|1\rangle$, - set ``control_value=1``. - * If the gate should execute when two control qubits are $|10\rangle$ - (binary 10), set ``control_value=0b10``. - - Defaults to all-ones (e.g., $2^N - 1$) if not provided. - - Attributes - ---------- - num_ctrl_qubits : int - The number of qubits acting as controls. - - target_gate : Gate - The gate to be applied to the target qubits. - """ - - __slots__ = ("arg_value", "arg_label", "_control_value") - num_ctrl_qubits: int | None = None - - def __init_subclass__(cls, **kwargs): - """ - Validates the subclass definition. - """ - - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): - return - - # 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 postive 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}" - ) - - # Automatically copy the validator from the target - if hasattr(cls, "target_gate") and hasattr( - cls.target_gate, "validate_params" - ): - cls.validate_params = staticmethod(cls.target_gate.validate_params) - - # Default set_inverse - # cls.self_inverse = cls.target_gate.self_inverse - - # In the circuit plot, only the target gate is shown. - # The control has its own symbol. - cls.latex_str = cls.target_gate.latex_str - - def __init__( - self, - arg_value: any = None, - control_value: int | None = None, - arg_label: str | None = None, - ) -> None: - if control_value is not None: - self._validate_control_value(control_value) - self._control_value = control_value - else: - self._control_value = (2 ** self.num_ctrl_qubits) - 1 - - if self.is_parametric_gate(): - ParametricGate.__init__( - self, arg_value=arg_value, arg_label=arg_label - ) - - @property - @abstractmethod - def target_gate() -> Gate: - pass - - @property - def self_inverse(self) -> int: - return self.target_gate.self_inverse - - @property - def control_value(self) -> int: - return self._control_value - - @classmethod - def _validate_control_value(cls, control_value: int) -> None: - """ - Internal validation for the control value. - - Raises - ------ - TypeError - If control_value is not an integer. - ValueError - If control_value is negative or exceeds the maximum value - possible for the number of control qubits ($2^N - 1$). - """ - - if type(control_value) is not int: - raise TypeError( - f"Control value must be an int, got {control_value}" - ) - - if control_value < 0 or control_value > 2**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 {control_value}" - ) - - def get_qobj(self) -> Qobj: - """ - Construct the full Qobj representation of the controlled gate. - - Returns - ------- - qobj : qutip.Qobj - The unitary matrix representing the controlled operation. - """ - target_gate = self.target_gate - if self.is_parametric_gate(): - target_gate = target_gate(self.arg_value) - - return controlled_gate( - U=target_gate.get_qobj(), - control_value=self.control_value, - ) - - @staticmethod - def is_controlled_gate() -> bool: - return True - - @classmethod - def is_parametric_gate(cls) -> bool: - return cls.target_gate.is_parametric_gate() - - def __str__(self) -> str: - return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" - - def __eq__(self, other) -> bool: - if type(self) is not type(other): - return False - - if self.control_value != other.control_value: - return False - return True - - -def unitary_gate( - gate_name: str, U: Qobj, user_namespace: str = "custom" -) -> Gate: +def unitary_gate(gate_name: str, U: Qobj, namespace: str = "custom") -> Gate: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ @@ -561,7 +271,7 @@ def unitary_gate( class _CustomGate(Gate): __slots__ = () name = gate_name - namespace = user_namespace + _namespace = namespace num_qubits = int(n) self_inverse = inverse @@ -570,35 +280,3 @@ def get_qobj(): return U return _CustomGate - - -def controlled( - gate: Gate, - n_ctrl_qubits: int = 1, - user_namespace: str = "custom", -) -> ControlledGate: - """ - Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. - """ - - class _CustomControlledGate(ControlledGate): - __slots__ = () - namespace = user_namespace - num_qubits = n_ctrl_qubits + gate.num_qubits - num_ctrl_qubits = n_ctrl_qubits - target_gate = gate - latex_str = rf"C{gate.name}" - - return _CustomControlledGate - - -class AngleParametricGate(ParametricGate): - __slots__ = () - - @staticmethod - def validate_params(arg_value): - for arg in arg_value: - try: - float(arg) - except TypeError: - raise ValueError(f"Invalid arg {arg} in arg_value") diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py new file mode 100644 index 000000000..fd4febd50 --- /dev/null +++ b/src/qutip_qip/operations/parametric.py @@ -0,0 +1,135 @@ +import inspect +from abc import abstractmethod +from collections.abc import Iterable + +from qutip import Qobj +from qutip_qip.operations import Gate + + +class ParametricGate(Gate): + r""" + Abstract base class for parametric quantum gates. + + Parameters + ---------- + arg_value : float or Sequence + The argument value(s) for the gate. If a single float is provided, + it is converted to a list. These values are saved as attributes + and can be accessed or modified later. + + arg_label : str, optional + Label for the argument to be shown in the circuit plot. + + Example: + If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit + plot will be rendered as ``$U(\phi)$``. + + Attributes + ---------- + num_params : int + The number of parameters required by the gate. This is a mandatory + class attribute for subclasses. + + arg_value : Sequence + The numerical values of the parameters provided to the gate. + + arg_label : str, optional + The LaTeX string representing the parameter variable in circuit plots. + + Raises + ------ + ValueError + If the number of provided arguments does not match `num_params`. + """ + + __slots__ = ("arg_value", "arg_label") + num_params: int + + def __init_subclass__(cls, **kwargs) -> None: + """ + Validates the subclass definition. + + Ensures that `num_params` is defined as a positive integer. + """ + super().__init_subclass__(**kwargs) + if inspect.isabstract(cls): + return + + # Assert num_params is a positive integer + num_params = getattr(cls, "num_params", None) + if (type(num_params) is not int) or (num_params < 1): + raise TypeError( + f"Class '{cls.__name__}' attribute 'num_params' must be a postive integer, " + f"got {type(num_params)} with value {num_params}." + ) + + def __init__(self, arg_value: float, arg_label: str | None = None): + if not isinstance(arg_value, Iterable): + arg_value = [arg_value] + + if len(arg_value) != self.num_params: + raise ValueError( + f"Requires {self.num_params} parameters, got {len(arg_value)}" + ) + + self.validate_params(arg_value) + self.arg_value = arg_value + self.arg_label = arg_label + + @staticmethod + @abstractmethod + def validate_params(arg_value): + r""" + Validate the provided parameters. + + This method should be implemented by subclasses to check if the + parameters are valid type and within valid range (e.g., $0 \le \theta < 2\pi$). + + Parameters + ---------- + arg_value : list of float + The parameters to validate. + """ + pass + + @abstractmethod + def get_qobj(self) -> Qobj: + """ + Get the QuTiP quantum object representation using the current parameters. + + Returns + ------- + qobj : qutip.Qobj + The unitary matrix representing the gate with the specific `arg_value`. + """ + pass + + @staticmethod + def is_parametric_gate(): + return True + + def __str__(self): + return f""" + Gate({self.name}, arg_value={self.arg_value}, + arg_label={self.arg_label}), + """ + + def __eq__(self, other) -> bool: + if type(self) is not type(other): + return False + + if self.arg_value != other.arg_value: + return False + return True + + +class AngleParametricGate(ParametricGate): + __slots__ = () + + @staticmethod + def validate_params(arg_value): + for arg in arg_value: + try: + float(arg) + except TypeError: + raise ValueError(f"Invalid arg {arg} in arg_value") diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index d3067c22f..9b854d3b2 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -139,7 +139,7 @@ def construct_circuit(self, angles): current_params = angles[i : i + n] if n > 0 else [] gate_instance = unitary_gate( gate_name=block.name, - user_namespace = "vqa", + namespace = "vqa", U=block.get_unitary(current_params), ) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index bb3729e4e..4d85b0efb 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -4,7 +4,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer from qutip_qip.operations import ( - ControlledGate, + controlled, IDLE, X, H, @@ -164,23 +164,9 @@ def qc3(): @pytest.fixture def qc4(): - class i(ControlledGate): - num_qubits = 2 - num_ctrl_qubits = 1 - target_gate = IDLE - - def __init__(self, control_value=1): - super().__init__(control_value=control_value) - - def get_qobj(self): - pass - - class ii(i): - num_qubits = 3 - num_ctrl_qubits = 2 - - class iii(i): - pass + i = controlled(IDLE, n_ctrl_qubits=1, gate_name="i") + ii = controlled(IDLE, n_ctrl_qubits=2, gate_name="ii") + iii = controlled(IDLE, n_ctrl_qubits=1, gate_name="iii") qc = QubitCircuit(5, num_cbits=2) qc.add_gate( From bffbae9628b7e584f3b2fb50a32448d03978c681 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 22 Feb 2026 19:45:14 +0530 Subject: [PATCH 013/117] run black --- src/qutip_qip/algorithms/qft.py | 5 +- src/qutip_qip/algorithms/qpe.py | 6 +- src/qutip_qip/circuit/_decompose.py | 117 ++++++++++++------ src/qutip_qip/circuit/circuit.py | 42 +++++-- src/qutip_qip/circuit/instruction.py | 5 +- src/qutip_qip/compiler/cavityqedcompiler.py | 5 +- src/qutip_qip/compiler/gatecompiler.py | 2 +- src/qutip_qip/compiler/spinchaincompiler.py | 5 +- src/qutip_qip/device/processor.py | 2 +- src/qutip_qip/operations/controlled.py | 4 +- src/qutip_qip/operations/gateclass.py | 4 +- src/qutip_qip/qasm.py | 4 +- src/qutip_qip/qiskit/utils/converter.py | 24 +++- src/qutip_qip/typing.py | 9 +- src/qutip_qip/vqa.py | 2 +- .../test_single_qubit_gate_decompositions.py | 17 +-- tests/test_circuit.py | 10 +- tests/test_compiler.py | 5 +- tests/test_gates.py | 24 ++-- tests/test_phase_flip.py | 2 +- tests/test_qasm.py | 10 +- tests/test_qft.py | 4 +- tests/test_qiskit.py | 14 ++- tests/test_qpe.py | 4 +- 24 files changed, 214 insertions(+), 112 deletions(-) diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index 2dca6fd27..666d845e0 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -113,7 +113,10 @@ def qft_gate_sequence(N=1, swapping=True, to_cnot=False): for j in range(i): if not to_cnot: qc.add_gate( - CPHASE(np.pi / (2 ** (i - j)), arg_label=r"{\pi/2^{%d}}" % (i - j)), + CPHASE( + np.pi / (2 ** (i - j)), + arg_label=r"{\pi/2^{%d}}" % (i - j), + ), targets=[j], controls=[i], ) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 35e5c012f..706ecc04b 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,9 +1,7 @@ import numpy as np from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence -from qutip_qip.operations import ( - unitary_gate, controlled, H, Gate -) +from qutip_qip.operations import unitary_gate, controlled, H, Gate def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): @@ -67,7 +65,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): controlled_u = controlled( gate=unitary_gate( gate_name=f"U^{power}", - namespace = "qpe", + namespace="qpe", U=U_power, ), ) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index a68922476..36aa3c534 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -53,7 +53,8 @@ def _gate_SQRTNOT(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 4) temp_resolved.add_gate( - RX(arg_value=np.pi / 2, arg_label=r"\pi/2"), targets=targets, + RX(arg_value=np.pi / 2, arg_label=r"\pi/2"), + targets=targets, ) @@ -63,10 +64,12 @@ def _gate_H(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=half_pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets, ) @@ -91,19 +94,24 @@ def _gate_CZ(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets, ) + _gate_CSIGN = _gate_CZ @@ -124,23 +132,29 @@ def _gate_ISWAP(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, targets=targets[1], controls=targets[0]) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RZ(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[1], + RZ(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets[0], + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets[0], ) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets[0], + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets[0], ) @@ -151,42 +165,53 @@ def _gate_FREDKIN(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(arg_value=pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(arg_value=pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], + RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(arg_value=pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(arg_value=pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], + RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=targets[1], + RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=targets[0], + RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], + RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=controls, + RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + targets=controls, ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[0], + RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + targets=targets[0], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( @@ -197,13 +222,16 @@ def _gate_FREDKIN(circ_instruction, temp_resolved): RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1] ) temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], + RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(arg_value=pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(arg_value=pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) temp_resolved.add_global_phase(phase=pi / 8) @@ -219,10 +247,12 @@ def _gate_TOFFOLI(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 8) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=controls[1], + RZ(arg_value=half_pi, arg_label=r"\pi/2"), + targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=controls[0], + RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + targets=controls[0], ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( @@ -231,35 +261,44 @@ def _gate_TOFFOLI(circ_instruction, temp_resolved): ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=controls[1], + RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=targets, + RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), targets=targets, + RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=targets, + RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), targets=targets, + RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(arg_value=half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(arg_value=np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_global_phase(phase=np.pi) @@ -291,8 +330,10 @@ def _basis_CZ(qc_temp, temp_resolved): style=circ_instruction.style, ) + _basis_CSIGN = _basis_CZ + def _basis_ISWAP(qc_temp, temp_resolved): half_pi = np.pi / 2 quarter_pi = np.pi / 4 diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 6dc6a01c2..1e42de213 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -67,7 +67,7 @@ def __init__( dims=None, num_cbits=0, user_gates=None, - N = None + N=None, ): # number of qubits in the register self._num_qubits = num_qubits @@ -89,12 +89,16 @@ def __init__( if input_states: self.input_states = input_states else: - self.input_states = [None for i in range(self.num_qubits + num_cbits)] + self.input_states = [ + None for i in range(self.num_qubits + num_cbits) + ] if output_states: self.output_states = output_states else: - self.output_states = [None for i in range(self.num_qubits + num_cbits)] + self.output_states = [ + None for i in range(self.num_qubits + num_cbits) + ] if user_gates is not None: raise ValueError( @@ -120,6 +124,7 @@ def gates(self) -> list[CircuitInstruction]: return self._instructions gates.setter + def gates(self) -> None: warnings.warn( "QubitCircuit.gates has been replaced with QubitCircuit.instructions", @@ -167,7 +172,7 @@ def add_state( self, state: str, targets: IntList, - state_type: str = "input", # FIXME Add an enum type hinting? + state_type: str = "input", # FIXME Add an enum type hinting? ): """ Add an input or output state to the circuit. By default all the input @@ -295,18 +300,18 @@ def add_gate( if arg_value is not None or arg_label is not None: warnings.warn( - "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(arg_value=np.pi)" \ + "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(arg_value=np.pi)" ", 'arg_value', 'arg_label' arguments will be removed from 'add_gate' method in the future version.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if control_value is not None: warnings.warn( - "Define 'control_value', in your Gate object e.g. CX(control_value=0)" \ + "Define 'control_value', in your Gate object e.g. CX(control_value=0)" ", 'control_value' argument will be removed from 'add_gate' method in the future version.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if isinstance(gate, GLOBALPHASE): @@ -346,7 +351,7 @@ def add_gate( warnings.warn( "Passing Gate as a string input has been deprecated and will be removed in future versions.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) gate_class = GATE_CLASS_MAP[gate] @@ -358,7 +363,10 @@ def add_gate( "or Gate class or its object instantiation" ) - if gate_class.is_controlled_gate() and gate_class.is_parametric_gate(): + if ( + gate_class.is_controlled_gate() + and gate_class.is_parametric_gate() + ): gate = gate_class( control_value=control_value, arg_value=arg_value, @@ -622,7 +630,15 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): measurements are added to the circuit") basis_1q_valid = ["RX", "RY", "RZ", "IDLE"] - basis_2q_valid = ["CNOT", "CX", "CSIGN", "CZ", "ISWAP", "SQRTSWAP", "SQRTISWAP"] + basis_2q_valid = [ + "CNOT", + "CX", + "CSIGN", + "CZ", + "ISWAP", + "SQRTSWAP", + "SQRTISWAP", + ] basis_1q = [] basis_2q = [] @@ -818,7 +834,9 @@ def propagators(self, expand=True, ignore_measurement=False): # For Circuit's Global Phase qobj = Qobj([self.global_phase]) if expand: - qobj = GLOBALPHASE(self.global_phase).get_qobj(num_qubits=self.num_qubits) + qobj = GLOBALPHASE(self.global_phase).get_qobj( + num_qubits=self.num_qubits + ) U_list.append(qobj) return U_list diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index 0c76bafc8..335e3dcbe 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -63,7 +63,10 @@ class GateInstruction(CircuitInstruction): def __post_init__(self) -> None: super().__post_init__() - if not (isinstance(self.operation, Gate) or issubclass(self.operation, Gate)): + if not ( + isinstance(self.operation, Gate) + or issubclass(self.operation, Gate) + ): raise TypeError(f"Operation must be a Gate, got {self.operation}") if len(self.qubits) != self.operation.num_qubits: diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 7056464cf..d4ea3d9d8 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -116,7 +116,10 @@ def _rotation_compiler( args["num_samples"], maximum=self.params[param_label][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] + / 2.0 + / np.pi + * 0.5, ) pulse_info = [(op_label + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index bfe2451ed..37f3e2afd 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -81,7 +81,7 @@ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): you can simply remove it. """, UserWarning, - stacklevel=2 + stacklevel=2, ) @property diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index e041727e0..fa431d9ff 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -140,7 +140,10 @@ def _rotation_compiler( args["num_samples"], maximum=self.params[param_label][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] + / 2.0 + / np.pi + * 0.5, ) pulse_info = [(op_label + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 9ddaf248b..9c235b054 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -1110,7 +1110,7 @@ def run_state( warnings.warn( "states will be deprecated and replaced by init_state", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if init_state is None and states is None: raise ValueError("Qubit state not defined.") diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 24a240869..1169a3f72 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -50,7 +50,7 @@ def __init_subclass__(cls, **kwargs): # Must have a target_gate target_gate = getattr(cls, "target_gate", None) - if (target_gate is None or not issubclass(target_gate, Gate)): + 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}." @@ -97,7 +97,7 @@ def __init__( self._validate_control_value(control_value) self._control_value = control_value else: - self._control_value = (2 ** self.num_ctrl_qubits) - 1 + self._control_value = (2**self.num_ctrl_qubits) - 1 if self.is_parametric_gate(): ParametricGate.__init__( diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index a352ffa8f..80da89b98 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -154,7 +154,7 @@ def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # Skip the below check for an abstract class - if inspect.isabstract(cls): + if inspect.isabstract(cls): return # If name attribute in subclass is not defined, set it to the name of the subclass @@ -260,7 +260,7 @@ def unitary_gate(gate_name: str, U: Qobj, namespace: str = "custom") -> Gate: """ n = np.log2(U.shape[0]) - inverse = (U == U.dag()) + inverse = U == U.dag() if n != np.log2(U.shape[1]): raise ValueError("The unitary U must be square.") diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 35e2b0401..6dd1053a9 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -635,7 +635,7 @@ def _add_qiskit_gates( ) elif name == "crz": qc.add_gate( - std.CRZ(arg_value = args), + std.CRZ(arg_value=args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -643,7 +643,7 @@ def _add_qiskit_gates( ) elif name == "cu1": qc.add_gate( - std.CPHASE(arg_value = args), + std.CPHASE(arg_value=args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index bf7afa79f..f4346f837 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -3,8 +3,26 @@ from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ( - X, Y, Z, H, S, T, RX, RY, RZ, SWAP, QASMU, PHASE, - CX, CY, CZ, CPHASE, CRX, CRY, CRZ, Gate + X, + Y, + Z, + H, + S, + T, + RX, + RY, + RZ, + SWAP, + QASMU, + PHASE, + CX, + CY, + CZ, + CPHASE, + CRX, + CRY, + CRZ, + Gate, ) # TODO Expand this dictionary for other gates like CS etc. @@ -129,7 +147,7 @@ def convert_qiskit_circuit_to_qutip( ) elif qiskit_instruction.name in _map_controlled_gates.keys(): - gate = _map_controlled_gates[qiskit_instruction.name] + gate = _map_controlled_gates[qiskit_instruction.name] if gate.is_parametric_gate(): gate = gate(arg_value) diff --git a/src/qutip_qip/typing.py b/src/qutip_qip/typing.py index 3d55aa9e9..c1eb3148f 100644 --- a/src/qutip_qip/typing.py +++ b/src/qutip_qip/typing.py @@ -3,7 +3,14 @@ import numpy as np __all__ = [ - "Int", "Real", "Number", "ScalarList", "IntList", "RealList", "ScalarList", "ArrayLike", + "Int", + "Real", + "Number", + "ScalarList", + "IntList", + "RealList", + "ScalarList", + "ArrayLike", ] # TODO When minimum version is updated to 3.12, use type (PEP 695) in place of TypeAlias diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 9b854d3b2..ebc433812 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -139,7 +139,7 @@ def construct_circuit(self, angles): current_params = angles[i : i + n] if n > 0 else [] gate_instance = unitary_gate( gate_name=block.name, - namespace = "vqa", + namespace="vqa", U=block.get_unitary(current_params), ) diff --git a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py index 1fad30195..ca53852a9 100644 --- a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py +++ b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py @@ -17,10 +17,9 @@ # TODO Add a custom gate - rand_unitary(2) + # Tests for private functions -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize( "method", [_ZYZ_rotation, _ZXZ_rotation, _ZYZ_pauli_X] ) @@ -38,9 +37,7 @@ def test_single_qubit_to_rotations(gate, method): assert np.isclose(fidelity_of_input_output, 1.0) -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize("method", ["ZXZ", "ZYZ", "ZYZ_PauliX"]) def test_check_single_qubit_to_decompose_to_rotations(gate, method): """Initial matrix and product of final decompositions are same within some @@ -57,9 +54,7 @@ def test_check_single_qubit_to_decompose_to_rotations(gate, method): assert np.isclose(fidelity_of_input_output, 1.0) -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize( "method", [_ZYZ_rotation, _ZXZ_rotation, _ZYZ_pauli_X] ) @@ -71,9 +66,7 @@ def test_output_is_tuple(gate, method): # Tests for public functions -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize("method", ["ZXZ", "ZYZ", "ZYZ_PauliX"]) def test_check_single_qubit_to_decompose_to_rotations_tuple(gate, method): """Initial matrix and product of final decompositions are same within some diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 56665307d..6068d584f 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -237,7 +237,10 @@ def test_add_circuit(self): qc1.instructions[i].cbits_ctrl_value == qc.instructions[i].cbits_ctrl_value ) - elif qc1.instructions[i].is_measurement_instruction() and qc.instructions[i].is_measurement_instruction(): + elif ( + qc1.instructions[i].is_measurement_instruction() + and qc.instructions[i].is_measurement_instruction() + ): assert qc1.instructions[i].cbits == qc.instructions[i].cbits # Test exception when qubit out of range @@ -703,7 +706,10 @@ def test_latex_code_classical_controls(self): @pytest.mark.parametrize( "valid_input, correct_result", - [(H_zyz_quantum_circuit, H), (sigmax_zyz_quantum_circuit, std.X.get_qobj())], + [ + (H_zyz_quantum_circuit, H), + (sigmax_zyz_quantum_circuit, std.X.get_qobj()), + ], ) def test_compute_unitary(self, valid_input, correct_result): final_output = valid_input.compute_unitary() diff --git a/tests/test_compiler.py b/tests/test_compiler.py index f3c26876a..77aa64f3c 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -137,7 +137,10 @@ def rx_compiler(self, circuit_instruction, args): 1000, maximum=args["params"]["sx"][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] + / 2.0 + / np.pi + * 0.5, ) pulse_info = [("sx" + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] diff --git a/tests/test_gates.py b/tests/test_gates.py index 6d1553def..6cd5a29c7 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -223,7 +223,9 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): for target in range(self.n_qubits): start = qutip.tensor(random[:target] + [base] + random[target:]) - test = expand_operator(gate.get_qobj(), self.n_qubits, target) * start + test = ( + expand_operator(gate.get_qobj(), self.n_qubits, target) * start + ) expected = qutip.tensor( random[:target] + [applied] + random[target:] ) @@ -244,17 +246,16 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): ], ) def test_two_qubit(self, gate, n_controls): - targets = [ qutip.rand_ket(2) for _ in [None] * 2] + targets = [qutip.rand_ket(2) for _ in [None] * 2] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(*targets) for q1, q2 in itertools.permutations(range(self.n_qubits), 2): qubits = others.copy() qubits[q1], qubits[q2] = targets - test = ( - expand_operator(gate.get_qobj(), dims=[2]*self.n_qubits, targets=[q1,q2]) - * qutip.tensor(*qubits) - ) + test = expand_operator( + gate.get_qobj(), dims=[2] * self.n_qubits, targets=[q1, q2] + ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement(qubits, reference, [q1, q2]) assert _infidelity(test, expected) < 1e-12 @@ -284,7 +285,9 @@ def test_three_qubit(self, gate: Gate, n_controls): for q1, q2, q3 in itertools.permutations(range(self.n_qubits), 3): qubits = others.copy() qubits[q1], qubits[q2], qubits[q3] = targets - test = expand_operator(gate.get_qobj(), self.n_qubits, [q1,q2,q3]) * qutip.tensor(*qubits) + test = expand_operator( + gate.get_qobj(), self.n_qubits, [q1, q2, q3] + ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement( qubits, reference, [q1, q2, q3] ) @@ -387,9 +390,7 @@ def test_non_qubit_systems(self, dimensions): ] expected = qutip.tensor(*operators) base_test = qutip.tensor(*[operators[x] for x in targets]) - test = expand_operator( - base_test, dims=dimensions, targets=targets - ) + test = expand_operator(base_test, dims=dimensions, targets=targets) assert test.dims == expected.dims np.testing.assert_allclose(test.full(), expected.full()) @@ -448,7 +449,8 @@ def test_gates_class(): circuit2.add_gate(std.T, targets=1) circuit2.add_gate(std.R([np.pi / 4, np.pi / 6]), targets=1) circuit2.add_gate( - std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0, + std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), + targets=0, ) circuit2.add_gate(std.CX, controls=0, targets=1) circuit2.add_gate(std.CPHASE(np.pi / 4), controls=0, targets=1) diff --git a/tests/test_phase_flip.py b/tests/test_phase_flip.py index 9b1075834..990a6bfd2 100644 --- a/tests/test_phase_flip.py +++ b/tests/test_phase_flip.py @@ -60,7 +60,7 @@ def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits): state = qc_encode.run(state) # Apply Z (phase-flip) error to qubit 1 - qc_error = QubitCircuit(num_qubits = 5) + qc_error = QubitCircuit(num_qubits=5) qc_error.add_gate(Z, targets=[1]) state = qc_error.run(state) diff --git a/tests/test_qasm.py b/tests/test_qasm.py index c99d753b3..e69b23299 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -75,9 +75,7 @@ def test_qasm_addcircuit(): check_gate_instruction_defn(qc.instructions[3], "CX", (1,), (0,)) check_gate_instruction_defn(qc.instructions[4], "H", (0,)) check_gate_instruction_defn(qc.instructions[5], "H", (1,)) - check_gate_instruction_defn( - qc.instructions[6], "H", (0,), (), (0, 1), 0 - ) + check_gate_instruction_defn(qc.instructions[6], "H", (0,), (), (0, 1), 0) check_measurement_defn(qc.instructions[7], "M", (0,), (0,)) check_measurement_defn(qc.instructions[8], "M", (1,), (1,)) @@ -88,7 +86,9 @@ def test_custom_gates(): qc = read_qasm(filepath) unitaries = qc.propagators() assert (unitaries[0] - unitaries[1]).norm() < 1e-12 - ry_cx = std.CX.get_qobj() * tensor(identity(2), std.RY(np.pi / 2).get_qobj()) + ry_cx = std.CX.get_qobj() * tensor( + identity(2), std.RY(np.pi / 2).get_qobj() + ) assert (unitaries[2] - ry_cx).norm() < 1e-12 @@ -177,12 +177,14 @@ def test_read_qasm_1(): filepath = Path(__file__).parent / "qasm_files" / filename read_qasm(filepath) + def test_read_qasm_2(): Gate.clear_cache(namespace="custom") filename2 = "w-state_with_comments.qasm" filepath2 = Path(__file__).parent / "qasm_files" / filename2 read_qasm(filepath2) + def test_parsing_mode(tmp_path): mode = "qiskit" qasm_input_string = ( diff --git a/tests/test_qft.py b/tests/test_qft.py index abcbe9bc3..4c11118dc 100644 --- a/tests/test_qft.py +++ b/tests/test_qft.py @@ -29,9 +29,7 @@ def testQFTGateSequenceNoSwapping(self): totsize = N * (N + 1) / 2 assert_equal(len(circuit.instructions), totsize) - snots = sum( - g.operation.name == "H" for g in circuit.instructions - ) + snots = sum(g.operation.name == "H" for g in circuit.instructions) assert_equal(snots, N) phases = sum( diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 78b3732b1..51e5237ed 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -58,20 +58,26 @@ def _compare_gate_instructions( req_gate.operation.name == res_gate.operation.name ) and ( list(req_gate.qubits) - == get_qutip_index(list(res_gate.qubits), result_circuit.num_qubits) + == get_qutip_index( + list(res_gate.qubits), result_circuit.num_qubits + ) ) if not check_condition: return False if req_gate.is_measurement_instruction(): - check_condition = list(req_gate.operation.classical_store) == get_qutip_index( + check_condition = list( + req_gate.operation.classical_store + ) == get_qutip_index( res_gate.operation.classical_store, result_circuit.num_cbits ) else: # TODO correct for float error in arg_value res_controls = None if res_gate.operation.is_controlled_gate(): - res_controls = get_qutip_index(list(res_gate.controls), result_circuit.num_qubits) + res_controls = get_qutip_index( + list(res_gate.controls), result_circuit.num_qubits + ) req_controls = None if req_gate.operation.is_controlled_gate(): @@ -150,7 +156,7 @@ def test_rotation_conversion(self): required_circuit.add_gate(H, targets=[2]) assert self._compare_circuit(result_circuit, required_circuit) - + def test_multiqubit_circuit_conversion(self): """ Test to check conversion of a circuit diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 6e2f61763..3eb74eb22 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -33,9 +33,7 @@ def test_controlled_unitary(self): )(control_value=1) assert_equal(controlled_u.control_value, 1) - assert_( - (controlled_u.target_gate.get_qobj() - U).norm() < 1e-12 - ) + assert_((controlled_u.target_gate.get_qobj() - U).norm() < 1e-12) def test_qpe_validation(self): """ From 4f24efb7c88e50bdaa88c96ab70fe85464fbf958 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 22 Feb 2026 19:49:21 +0530 Subject: [PATCH 014/117] Removed get_qobj() static method from std controlled gates --- src/qutip_qip/operations/std/other_gates.py | 48 +++------------- .../operations/std/two_qubit_gate.py | 55 ------------------- tests/test_gates.py | 22 ++++---- tests/test_optpulseprocessor.py | 2 +- tests/test_qasm.py | 2 +- 5 files changed, 21 insertions(+), 108 deletions(-) diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/std/other_gates.py index 900920260..827e5bb2e 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/std/other_gates.py @@ -59,28 +59,12 @@ class TOFFOLI(ControlledGate): [0. 0. 0. 0. 0. 0. 1. 0.]] """ - latex_str = r"{\rm TOFFOLI}" - target_gate = X - - num_qubits: int = 3 - num_ctrl_qubits: int = 2 __slots__ = () + num_qubits: int = 3 - @staticmethod - def get_qobj() -> Qobj: - return Qobj( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 1, 0], - ], - dims=[[2, 2, 2], [2, 2, 2]], - ) + num_ctrl_qubits: int = 2 + target_gate = X + latex_str = r"{\rm TOFFOLI}" class FREDKIN(ControlledGate): @@ -103,25 +87,9 @@ class FREDKIN(ControlledGate): [0. 0. 0. 0. 0. 0. 0. 1.]] """ - latex_str = r"{\rm FREDKIN}" - target_gate = SWAP - - num_qubits: int = 3 - num_ctrl_qubits: int = 1 __slots__ = () + num_qubits: int = 3 - @staticmethod - def get_qobj() -> Qobj: - return Qobj( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 1, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - ], - dims=[[2, 2, 2], [2, 2, 2]], - ) + num_ctrl_qubits: int = 1 + target_gate = SWAP + latex_str = r"{\rm FREDKIN}" diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 4ead4e62b..07616678d 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -380,13 +380,6 @@ class CX(_ControlledTwoQubitGate): is_clifford = True latex_str = r"{\rm CNOT}" - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], - dims=[[2, 2], [2, 2]], - ) - class CNOT(CX): __slots__ = () @@ -423,13 +416,6 @@ class CY(_ControlledTwoQubitGate): is_clifford = True latex_str = r"{\rm CY}" - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], - dims=[[2, 2], [2, 2]], - ) - class CZ(_ControlledTwoQubitGate): """ @@ -453,13 +439,6 @@ class CZ(_ControlledTwoQubitGate): is_clifford = True latex_str = r"{\rm CZ}" - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], - dims=[[2, 2], [2, 2]], - ) - class CSIGN(CZ): __slots__ = () @@ -497,19 +476,6 @@ class CH(_ControlledTwoQubitGate): target_gate = H latex_str = r"{\rm CH}" - @staticmethod - def get_qobj(): - sq_2 = 1 / np.sqrt(2) - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, sq_2, sq_2], - [0, 0, sq_2, -sq_2], - ], - dims=[[2, 2], [2, 2]], - ) - class CT(_ControlledTwoQubitGate): r""" @@ -534,18 +500,6 @@ class CT(_ControlledTwoQubitGate): target_gate = T latex_str = r"{\rm CT}" - @staticmethod - def get_qobj(): - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, (1 + 1j) / np.sqrt(2)], - ], - dims=[[2, 2], [2, 2]], - ) - class CS(_ControlledTwoQubitGate): r""" @@ -570,15 +524,6 @@ class CS(_ControlledTwoQubitGate): target_gate = S latex_str = r"{\rm CS}" - @staticmethod - def get_qobj(): - return Qobj( - np.array( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] - ), - dims=[[2, 2], [2, 2]], - ) - class CPHASE(_ControlledTwoQubitGate): r""" diff --git a/tests/test_gates.py b/tests/test_gates.py index 6cd5a29c7..ad0a5c59a 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -104,7 +104,7 @@ def test_swap(self): ) def test_toffoli(self, permutation): test = expand_operator( - std.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation + std.TOFFOLI().get_qobj(), dims=[2] * 3, targets=permutation ) base = qutip.tensor( 1 - qutip.basis([2, 2], [1, 1]).proj(), qutip.qeye(2) @@ -234,11 +234,11 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.CX, 1, id="CX"), - pytest.param(std.CY, 1, id="CY"), - pytest.param(std.CZ, 1, id="CZ"), - pytest.param(std.CS, 1, id="CS"), - pytest.param(std.CT, 1, id="CT"), + pytest.param(std.CX(), 1, id="CX"), + pytest.param(std.CY(), 1, id="CY"), + pytest.param(std.CZ(), 1, id="CZ"), + pytest.param(std.CS(), 1, id="CS"), + pytest.param(std.CT(), 1, id="CT"), pytest.param(std.SWAP, 0, id="SWAP"), pytest.param(std.ISWAP, 0, id="ISWAP"), pytest.param(std.SQRTSWAP, 0, id="SQRTSWAP"), @@ -272,8 +272,8 @@ def test_two_qubit(self, gate, n_controls): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.FREDKIN, 1, id="Fredkin"), - pytest.param(std.TOFFOLI, 2, id="Toffoli"), + pytest.param(std.FREDKIN(), 1, id="Fredkin"), + pytest.param(std.TOFFOLI(), 2, id="Toffoli"), # pytest.param(RandomThreeQubitGate(), 2, id="random"), ], ) @@ -336,7 +336,7 @@ def test_general_qubit_expansion(self, n_targets): def test_cnot_explicit(self): test = expand_operator( - std.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] + std.CX().get_qobj(), dims=[2] * 3, targets=[2, 0] ).full() expected = np.array( [ @@ -395,10 +395,10 @@ def test_non_qubit_systems(self, dimensions): np.testing.assert_allclose(test.full(), expected.full()) def test_dtype(self): - expanded_qobj = expand_operator(std.CX.get_qobj(), dims=[2, 2, 2]).data + expanded_qobj = expand_operator(std.CX().get_qobj(), dims=[2, 2, 2]).data assert isinstance(expanded_qobj, qutip.data.CSR) expanded_qobj = expand_operator( - std.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" + std.CX().get_qobj(), dims=[2, 2, 2], dtype="dense" ).data assert isinstance(expanded_qobj, qutip.data.Dense) diff --git a/tests/test_optpulseprocessor.py b/tests/test_optpulseprocessor.py index a64bf5530..0597cbf91 100644 --- a/tests/test_optpulseprocessor.py +++ b/tests/test_optpulseprocessor.py @@ -64,7 +64,7 @@ def test_multi_qubits(self): test.add_control(sigmay(), cyclic_permutation=True) # test pulse genration for cnot gate, with kwargs - qc = [tensor([identity(2), CX.get_qobj()])] + qc = [tensor([identity(2), CX().get_qobj()])] test.load_circuit( qc, num_tslots=num_tslots, evo_time=evo_time, min_fid_err=1.0e-6 ) diff --git a/tests/test_qasm.py b/tests/test_qasm.py index e69b23299..13a64bfee 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -86,7 +86,7 @@ def test_custom_gates(): qc = read_qasm(filepath) unitaries = qc.propagators() assert (unitaries[0] - unitaries[1]).norm() < 1e-12 - ry_cx = std.CX.get_qobj() * tensor( + ry_cx = std.CX().get_qobj() * tensor( identity(2), std.RY(np.pi / 2).get_qobj() ) assert (unitaries[2] - ry_cx).norm() < 1e-12 From a4594cfe36b7a190caba19c7bc805019d5213efb Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 22 Feb 2026 20:09:05 +0530 Subject: [PATCH 015/117] Corrected a bug in controlled_gate for beyond two qubit controlled gates --- doc/source/apidoc/qutip_qip.operations.rst | 2 +- src/qutip_qip/algorithms/qpe.py | 4 ++-- src/qutip_qip/operations/__init__.py | 8 ++++---- src/qutip_qip/operations/controlled.py | 7 ++++--- src/qutip_qip/operations/utils.py | 22 ++++++---------------- tests/test_gates.py | 4 +++- tests/test_qpe.py | 4 ++-- tests/test_renderer.py | 8 ++++---- 8 files changed, 26 insertions(+), 33 deletions(-) diff --git a/doc/source/apidoc/qutip_qip.operations.rst b/doc/source/apidoc/qutip_qip.operations.rst index 3e9d900eb..6b0fc98db 100644 --- a/doc/source/apidoc/qutip_qip.operations.rst +++ b/doc/source/apidoc/qutip_qip.operations.rst @@ -50,6 +50,6 @@ qutip\_qip.operations .. autosummary:: - controlled_gate + controlled_gate_unitary expand_operator gate_sequence_product diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 706ecc04b..9d43cd64a 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,7 @@ import numpy as np from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence -from qutip_qip.operations import unitary_gate, controlled, H, Gate +from qutip_qip.operations import unitary_gate, controlled_gate, H, Gate def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): @@ -62,7 +62,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): U_power = U if power == 1 else U**power # Add controlled-U^power gate - controlled_u = controlled( + controlled_u = controlled_gate( gate=unitary_gate( gate_name=f"U^{power}", namespace="qpe", diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index cb13cd330..38bdc1e68 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -5,11 +5,11 @@ from .utils import ( expand_operator, gate_sequence_product, - controlled_gate, + controlled_gate_unitary, ) from .gateclass import Gate, unitary_gate from .parametric import ParametricGate, AngleParametricGate -from .controlled import ControlledGate, controlled +from .controlled import ControlledGate, controlled_gate from .measurement import Measurement from .gates import ( rx, @@ -139,7 +139,7 @@ "ParametricGate", "ControlledGate", "unitary_gate", - "controlled", + "controlled_gate", "AngleParametricGate", "Measurement", "GATE_CLASS_MAP", @@ -174,7 +174,7 @@ "molmer_sorensen", "toffoli", "rotation", - "controlled_gate", + "controlled_gate_unitary", "globalphase", "hadamard_transform", "qubit_clifford_group", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 1169a3f72..fca3695ee 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -2,7 +2,7 @@ from abc import abstractmethod from qutip import Qobj -from qutip_qip.operations import Gate, ParametricGate, controlled_gate +from qutip_qip.operations import Gate, ParametricGate, controlled_gate_unitary class ControlledGate(Gate): @@ -155,8 +155,9 @@ def get_qobj(self) -> Qobj: if self.is_parametric_gate(): target_gate = target_gate(self.arg_value) - return controlled_gate( + return controlled_gate_unitary( U=target_gate.get_qobj(), + num_controls=self.num_ctrl_qubits, control_value=self.control_value, ) @@ -180,7 +181,7 @@ def __eq__(self, other) -> bool: return True -def controlled( +def controlled_gate( gate: Gate, n_ctrl_qubits: int = 1, gate_name: str = "_CustomControlledGate", diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 9f3dcff74..e6ddd4a2d 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -252,12 +252,10 @@ def gate_sequence_product( return gate_sequence_product_with_expansion(U_list, left_to_right) -def controlled_gate( +def controlled_gate_unitary( U: Qobj, - controls: int | Sequence[int] = 0, - targets: int | Sequence[int] = 1, - N: int | None = None, - control_value: int = 1, + num_controls: int, + control_value: int, ) -> Qobj: """ Create an N-qubit controlled gate from a single-qubit gate U with the given @@ -282,25 +280,17 @@ def controlled_gate( Quantum object representing the controlled-U gate. """ # Compatibility - if not isinstance(controls, Iterable): - controls = [controls] - if not isinstance(targets, Iterable): - targets = [targets] - num_controls = len(controls) num_targets = len(U.dims[0]) - N = num_controls + num_targets if N is None else N # First, assume that the last qubit is the target and control qubits are # in the increasing order. # The control_value is the location of this unitary. - block_matrices = [np.array([[1, 0], [0, 1]])] * 2**num_controls + target_dim = U.shape[0] + block_matrices = [np.eye(target_dim) for _ in range(2**num_controls)] block_matrices[control_value] = U.full() result = block_diag(*block_matrices) result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) # Expand it to N qubits and permute qubits labelling - if controls + targets == list(range(N)): - return result - else: - return expand_operator(result, targets=controls + targets) + return result diff --git a/tests/test_gates.py b/tests/test_gates.py index ad0a5c59a..c983dd98e 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -395,7 +395,9 @@ def test_non_qubit_systems(self, dimensions): np.testing.assert_allclose(test.full(), expected.full()) def test_dtype(self): - expanded_qobj = expand_operator(std.CX().get_qobj(), dims=[2, 2, 2]).data + expanded_qobj = expand_operator( + std.CX().get_qobj(), dims=[2, 2, 2] + ).data assert isinstance(expanded_qobj, qutip.data.CSR) expanded_qobj = expand_operator( std.CX().get_qobj(), dims=[2, 2, 2], dtype="dense" diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 3eb74eb22..b23254e5c 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -2,7 +2,7 @@ from numpy.testing import assert_, assert_equal import unittest from qutip import Qobj, sigmaz, tensor -from qutip_qip.operations import controlled, unitary_gate +from qutip_qip.operations import controlled_gate, unitary_gate from qutip_qip.algorithms.qpe import qpe @@ -28,7 +28,7 @@ def test_controlled_unitary(self): """ U = Qobj([[0, 1], [1, 0]]) - controlled_u = controlled( + controlled_u = controlled_gate( gate=unitary_gate(gate_name="CU", U=U), )(control_value=1) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 4d85b0efb..e610aa3d6 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -4,7 +4,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer from qutip_qip.operations import ( - controlled, + controlled_gate, IDLE, X, H, @@ -164,9 +164,9 @@ def qc3(): @pytest.fixture def qc4(): - i = controlled(IDLE, n_ctrl_qubits=1, gate_name="i") - ii = controlled(IDLE, n_ctrl_qubits=2, gate_name="ii") - iii = controlled(IDLE, n_ctrl_qubits=1, gate_name="iii") + i = controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="i") + ii = controlled_gate(IDLE, n_ctrl_qubits=2, gate_name="ii") + iii = controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="iii") qc = QubitCircuit(5, num_cbits=2) qc.add_gate( From 1b0437b165cf5d26937fbd605d03b96add36a20d Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 01:07:01 +0530 Subject: [PATCH 016/117] Added some other standard gates --- src/qutip_qip/operations/__init__.py | 52 ++------- src/qutip_qip/operations/controlled.py | 2 +- src/qutip_qip/operations/gateclass.py | 7 +- src/qutip_qip/operations/std/__init__.py | 52 +++++++++ .../operations/std/single_qubit_gate.py | 109 ++++++++++++++++-- .../operations/std/two_qubit_gate.py | 44 ++++++- 6 files changed, 209 insertions(+), 57 deletions(-) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 38bdc1e68..623190222 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -59,8 +59,11 @@ SNOT, SQRTNOT, SQRTX, + SQRTXdag, S, + Sdag, T, + Tdag, R, QASMU, SWAP, @@ -88,52 +91,9 @@ FREDKIN, GLOBALPHASE, IDLE, + GATE_CLASS_MAP, ) -GATE_CLASS_MAP: dict[str, Gate] = { - "GLOBALPHASE": GLOBALPHASE, - "IDLE": IDLE, - "X": X, - "Y": Y, - "Z": Z, - "RX": RX, - "RY": RY, - "RZ": RZ, - "H": H, - "SNOT": SNOT, - "SQRTNOT": SQRTX, - "SQRTX": SQRTX, - "S": S, - "T": T, - "R": R, - "QASMU": QASMU, - "SWAP": SWAP, - "ISWAP": ISWAP, - "iSWAP": ISWAP, - "CNOT": CX, - "SQRTSWAP": SQRTSWAP, - "SQRTISWAP": SQRTISWAP, - "SWAPALPHA": SWAPALPHA, - "SWAPalpha": SWAPALPHA, - "BERKELEY": BERKELEY, - "MS": MS, - "TOFFOLI": TOFFOLI, - "FREDKIN": FREDKIN, - "CSIGN": CZ, - "CRX": CRX, - "CRY": CRY, - "CRZ": CRZ, - "CX": CX, - "CY": CY, - "CZ": CZ, - "CS": CS, - "CT": CT, - "CH": CH, - "CPHASE": CPHASE, - "RZX": RZX, - "CQASMU": CQASMU, -} - __all__ = [ "Gate", "ParametricGate", @@ -191,8 +151,11 @@ "SNOT", "SQRTNOT", "SQRTX", + "SQRTXdag", "S", + "Sdag", "T", + "Tdag", "R", "QASMU", "SWAP", @@ -219,4 +182,5 @@ "CPHASE", "RZX", "CQASMU", + "IDLE", ] diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index fca3695ee..7e829fbcb 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -81,7 +81,7 @@ def __init_subclass__(cls, **kwargs): cls.validate_params = staticmethod(cls.target_gate.validate_params) # Default set_inverse - # cls.self_inverse = cls.target_gate.self_inverse + cls.self_inverse = cls.target_gate.self_inverse # In the circuit plot, only the target gate is shown. # The control has its own symbol. diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 80da89b98..394e8914d 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -15,7 +15,6 @@ class _GateMetaClass(ABCMeta): "num_ctrl_qubits", "num_params", "target_gate", - "self_inverse", "is_clifford", ) ) @@ -280,3 +279,9 @@ def get_qobj(): return U return _CustomGate + +def inverse_gate(gate_name: str, gate: Gate, namespace: str): + class _InverseGate(Gate): + pass + + return _InverseGate \ No newline at end of file diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/std/__init__.py index acd844d5c..4c53c0322 100644 --- a/src/qutip_qip/operations/std/__init__.py +++ b/src/qutip_qip/operations/std/__init__.py @@ -10,8 +10,11 @@ SNOT, SQRTNOT, SQRTX, + SQRTXdag, S, + Sdag, T, + Tdag, R, QASMU, IDLE, @@ -45,7 +48,53 @@ FREDKIN, ) +GATE_CLASS_MAP = { + "GLOBALPHASE": GLOBALPHASE, + "IDLE": IDLE, + "X": X, + "Y": Y, + "Z": Z, + "RX": RX, + "RY": RY, + "RZ": RZ, + "H": H, + "SNOT": SNOT, + "SQRTNOT": SQRTX, + "SQRTX": SQRTX, + "S": S, + "T": T, + "R": R, + "QASMU": QASMU, + "SWAP": SWAP, + "ISWAP": ISWAP, + "iSWAP": ISWAP, + "CNOT": CX, + "SQRTSWAP": SQRTSWAP, + "SQRTISWAP": SQRTISWAP, + "SWAPALPHA": SWAPALPHA, + "SWAPalpha": SWAPALPHA, + "BERKELEY": BERKELEY, + "MS": MS, + "TOFFOLI": TOFFOLI, + "FREDKIN": FREDKIN, + "CSIGN": CZ, + "CRX": CRX, + "CRY": CRY, + "CRZ": CRZ, + "CX": CX, + "CY": CY, + "CZ": CZ, + "CS": CS, + "CT": CT, + "CH": CH, + "CPHASE": CPHASE, + "RZX": RZX, + "CQASMU": CQASMU, +} + + __all__ = [ + "GATE_CLASS_MAP", "IDLE", "X", "Y", @@ -58,8 +107,11 @@ "SNOT", "SQRTNOT", "SQRTX", + "SQRTXdag", "S", + "Sdag", "T", + "Tdag", "R", "QASMU", "SWAP", diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index b16edece1..6951deb40 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -51,7 +51,7 @@ class Y(_SingleQubitGate): Examples -------- >>> from qutip_qip.operations import Y - >>> Y(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> Y.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = [[0.+0.j 0.-1.j] @@ -76,7 +76,7 @@ class Z(_SingleQubitGate): Examples -------- >>> from qutip_qip.operations import Z - >>> Z(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> Z.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = [[ 1. 0.] @@ -121,7 +121,7 @@ class H(_SingleQubitGate): Examples -------- >>> from qutip_qip.operations import H - >>> H(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> H.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = [[ 0.70711 0.70711] @@ -140,6 +140,9 @@ def get_qobj(): class SNOT(H): + """ + Hadamard gate (Deprecated, use H instead). + """ __slots__ = () def __init__(self): @@ -158,8 +161,8 @@ class SQRTX(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTNOT - >>> SQRTNOT(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations import SQRTX + >>> SQRTX.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[0.5+0.5j 0.5-0.5j] @@ -173,9 +176,42 @@ class SQRTX(_SingleQubitGate): latex_str = r"\sqrt{\rm NOT}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) + @staticmethod + def inverse() -> Gate: + return SQRTXdag + + +class SQRTXdag(_SingleQubitGate): + r""" + :math:`\sqrt{X}^{\dag}` gate. + + Examples + -------- + >>> from qutip_qip.operations import SQRTXdag + >>> SQRTXdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.5-0.5j 0.5+0.5j] + [0.5+0.5j 0.5-0.5j]] + """ + + __slots__ = () + + self_inverse = False + is_clifford = True + latex_str = r"\sqrt{\rm Xdag}" + + @staticmethod + def get_qobj() -> Qobj: + return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) + + @staticmethod + def inverse() -> Gate: + return SQRTX + class SQRTNOT(SQRTX): __slots__ = () @@ -197,7 +233,7 @@ class S(_SingleQubitGate): Examples -------- >>> from qutip_qip.operations import S - >>> S(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> S.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[1.+0.j 0.+0.j] @@ -211,9 +247,42 @@ class S(_SingleQubitGate): latex_str = r"{\rm S}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return Qobj([[1, 0], [0, 1j]]) + @staticmethod + def inverse() -> Gate: + return Sdag + + +class Sdag(_SingleQubitGate): + r""" + S gate or :math:`\sqrt{Z}` gate. + + Examples + -------- + >>> from qutip_qip.operations import S + >>> Sdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j] + [0.+0.j 0.-1.j]] + """ + + __slots__ = () + + self_inverse = False + is_clifford = True + latex_str = r"{\rm Sdag}" + + @staticmethod + def get_qobj() -> Qobj: + return Qobj([[1, 0], [0, -1j]]) + + @staticmethod + def inverse() -> Gate: + return S + class T(_SingleQubitGate): r""" @@ -239,6 +308,30 @@ def get_qobj(): return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) +class Tdag(_SingleQubitGate): + r""" + Tdag gate or :math:`\sqrt[4]{Z}^{\dag}` gate. + + Examples + -------- + >>> from qutip_qip.operations import Tdag + >>> Tdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j ] + [0. +0.j 0.70711-0.70711j]] + """ + + __slots__ = () + + self_inverse = False + latex_str = r"{\rm Tdag}" + + @staticmethod + def get_qobj(): + return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) + + class RX(_SingleQubitParametricGate): """ Single-qubit rotation RX. diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 07616678d..e82cf0728 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -35,7 +35,7 @@ class SWAP(_TwoQubitGate): Examples -------- >>> from qutip_qip.operations import SWAP - >>> SWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> SWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0.] @@ -65,7 +65,7 @@ class ISWAP(_TwoQubitGate): Examples -------- >>> from qutip_qip.operations import ISWAP - >>> ISWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> ISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] @@ -81,12 +81,50 @@ class ISWAP(_TwoQubitGate): latex_str = r"{i}{\rm SWAP}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], dims=[[2, 2], [2, 2]], ) + @staticmethod + def inverse() -> Gate: + return ISWAPdag + + +class ISWAPdag(_TwoQubitGate): + """ + iSWAPdag gate. + + Examples + -------- + >>> from qutip_qip.operations import ISWAPdag + >>> ISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.-1.j 0.+0.j] + [0.+0.j 0.-1.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 1.+0.j]] + """ + + __slots__ = () + + self_inverse = False + is_clifford = True + latex_str = r"{i}{\rm SWAPdag}" + + @staticmethod + def get_qobj() -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 0, -1j, 0], [0, -1j, 0, 0], [0, 0, 0, 1]], + dims=[[2, 2], [2, 2]], + ) + + @staticmethod + def inverse() -> Gate: + return ISWAP + class SQRTSWAP(_TwoQubitGate): r""" From c64502602d888807b91b0538107f1d13feae60e3 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 04:40:58 +0530 Subject: [PATCH 017/117] Added inverse for single qubit gates --- src/qutip_qip/operations/parametric.py | 2 +- .../operations/std/single_qubit_gate.py | 32 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index fd4febd50..eaa4d14c7 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -73,7 +73,7 @@ def __init__(self, arg_value: float, arg_label: str | None = None): ) self.validate_params(arg_value) - self.arg_value = arg_value + self.arg_value = list(arg_value) self.arg_label = arg_label @staticmethod diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 6951deb40..35f83f0b4 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -291,7 +291,7 @@ class T(_SingleQubitGate): Examples -------- >>> from qutip_qip.operations import T - >>> T(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> T.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[1. +0.j 0. +0.j ] @@ -361,6 +361,10 @@ def get_qobj(self): dims=[[2], [2]], ) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return RX(-theta) + class RY(_SingleQubitParametricGate): """ @@ -369,7 +373,7 @@ class RY(_SingleQubitParametricGate): Examples -------- >>> from qutip_qip.operations import RY - >>> RY(0, 3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> RY(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[ 0.70711 -0.70711] @@ -390,6 +394,10 @@ def get_qobj(self): ] ) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return RY(-theta) + class RZ(_SingleQubitParametricGate): """ @@ -398,7 +406,7 @@ class RZ(_SingleQubitParametricGate): Examples -------- >>> from qutip_qip.operations import RZ - >>> RZ(0, 3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> RZ(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[0.70711-0.70711j 0. +0.j ] @@ -414,6 +422,10 @@ def get_qobj(self): phi = self.arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return RZ(-theta) + class PHASE(_SingleQubitParametricGate): """ @@ -438,6 +450,10 @@ def get_qobj(self): ] ) + def inverse(self) -> Gate: + phi = self.arg_value[0] + return PHASE(-phi) + class R(_SingleQubitParametricGate): r""" @@ -453,7 +469,7 @@ class R(_SingleQubitParametricGate): Examples -------- >>> from qutip_qip.operations import R - >>> R(0, (np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + >>> R((np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[ 0.70711 -0.70711] @@ -480,6 +496,10 @@ def get_qobj(self): ] ) + def inverse(self) -> Gate: + theta, phi = self.arg_value + return R([-theta, -phi]) + class QASMU(_SingleQubitParametricGate): r""" @@ -517,3 +537,7 @@ def get_qobj(self): ], ] ) + + def inverse(self) -> Gate: + theta, phi, gamma = self.arg_value + return QASMU([-theta, -phi, -gamma]) From 72799576f33d31adf4f9f1f759d589185310367f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 15:07:12 +0530 Subject: [PATCH 018/117] Added tests for gate inverse of single qubit gate --- .../operations/std/single_qubit_gate.py | 40 +++++++++++-------- tests/test_gates.py | 18 +++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 35f83f0b4..34c57b875 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -40,7 +40,7 @@ class X(_SingleQubitGate): latex_str = r"X" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return sigmax(dtype="dense") @@ -65,7 +65,7 @@ class Y(_SingleQubitGate): latex_str = r"Y" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return sigmay(dtype="dense") @@ -90,7 +90,7 @@ class Z(_SingleQubitGate): latex_str = r"Z" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return sigmaz(dtype="dense") @@ -110,7 +110,7 @@ class IDLE(_SingleQubitGate): latex_str = r"{\rm IDLE}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return qeye(2) @@ -135,7 +135,7 @@ class H(_SingleQubitGate): latex_str = r"H" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return 1 / np.sqrt(2.0) * Qobj([[1, 1], [1, -1]]) @@ -304,9 +304,13 @@ class T(_SingleQubitGate): latex_str = r"{\rm T}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) + @staticmethod + def inverse() -> Gate: + return Tdag + class Tdag(_SingleQubitGate): r""" @@ -328,9 +332,13 @@ class Tdag(_SingleQubitGate): latex_str = r"{\rm Tdag}" @staticmethod - def get_qobj(): + def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) + @staticmethod + def inverse() -> Gate: + return T + class RX(_SingleQubitParametricGate): """ @@ -351,7 +359,7 @@ class RX(_SingleQubitParametricGate): num_params = 1 latex_str = r"R_x" - def get_qobj(self): + def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj( [ @@ -385,7 +393,7 @@ class RY(_SingleQubitParametricGate): num_params = 1 latex_str = r"R_y" - def get_qobj(self): + def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj( [ @@ -418,7 +426,7 @@ class RZ(_SingleQubitParametricGate): num_params = 1 latex_str = r"R_z" - def get_qobj(self): + def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) @@ -441,7 +449,7 @@ class PHASE(_SingleQubitParametricGate): num_params = 1 latex_str = r"PHASE" - def get_qobj(self): + def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj( [ @@ -481,7 +489,7 @@ class R(_SingleQubitParametricGate): num_params = 2 latex_str = r"{\rm R}" - def get_qobj(self): + def get_qobj(self) -> Qobj: phi, theta = self.arg_value return Qobj( [ @@ -497,8 +505,8 @@ def get_qobj(self): ) def inverse(self) -> Gate: - theta, phi = self.arg_value - return R([-theta, -phi]) + phi, theta = self.arg_value + return R([phi, -theta]) class QASMU(_SingleQubitParametricGate): @@ -523,7 +531,7 @@ class QASMU(_SingleQubitParametricGate): num_params = 3 latex_str = r"{\rm QASMU}" - def get_qobj(self): + def get_qobj(self) -> Qobj: theta, phi, gamma = self.arg_value return Qobj( [ @@ -540,4 +548,4 @@ def get_qobj(self): def inverse(self) -> Gate: theta, phi, gamma = self.arg_value - return QASMU([-theta, -phi, -gamma]) + return QASMU([-theta, -gamma, -phi]) diff --git a/tests/test_gates.py b/tests/test_gates.py index c983dd98e..e0657f7f6 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -470,3 +470,21 @@ def test_gates_class(): result2 = circuit2.run(init_state) assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 + +GATES = [std.X, std.Y, std.Z, std.H, std.S, std.T, std.SWAP, std.ISWAP] +PARAMETRIC_GATE = [ + std.RX(0.5), std.RY(0.5), std.RZ(0.5), std.PHASE(0.5), + std.R((0.5, 0.9)), std.QASMU((0.1, 0.2, 0.3)) +] + +@pytest.mark.parametrize( + "gate", GATES + PARAMETRIC_GATE +) +def test_gate_inverse(gate: Gate): + n = 2**gate.num_qubits + inverse_gate = gate.inverse() + np.testing.assert_allclose( + (gate.get_qobj() * inverse_gate.get_qobj()).full(), + np.eye(n), + atol=1e-12 + ) \ No newline at end of file From 46b7f1250d60b4d46eb8774f74639d5978fcc6df Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 15:28:22 +0530 Subject: [PATCH 019/117] Added gate inverse and tests for non-controlled two qubit gates --- src/qutip_qip/operations/std/__init__.py | 26 ++- .../operations/std/two_qubit_gate.py | 175 ++++++++++++++++-- tests/test_gates.py | 12 +- 3 files changed, 193 insertions(+), 20 deletions(-) diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/std/__init__.py index 4c53c0322..70957fc11 100644 --- a/src/qutip_qip/operations/std/__init__.py +++ b/src/qutip_qip/operations/std/__init__.py @@ -22,10 +22,14 @@ from .two_qubit_gate import ( SWAP, ISWAP, + ISWAPdag, SQRTSWAP, + SQRTSWAPdag, SQRTISWAP, - SWAPALPHA, + SQRTISWAPdag, BERKELEY, + BERKELEYdag, + SWAPALPHA, MS, RZX, CX, @@ -59,21 +63,26 @@ "RZ": RZ, "H": H, "SNOT": SNOT, - "SQRTNOT": SQRTX, + "SQRTNOT": SQRTNOT, "SQRTX": SQRTX, + "SQRTXdag": SQRTXdag, "S": S, + "Sdag": Sdag, "T": T, + "Tdag": Tdag, "R": R, "QASMU": QASMU, "SWAP": SWAP, "ISWAP": ISWAP, - "iSWAP": ISWAP, - "CNOT": CX, + "ISWAPdag": ISWAPdag, "SQRTSWAP": SQRTSWAP, + "SQRTSWAPdag": SQRTSWAPdag, "SQRTISWAP": SQRTISWAP, + "SQRTISWAPdag": SQRTISWAPdag, + "BERKELEY": BERKELEY, + "BERKELEYdag": BERKELEYdag, "SWAPALPHA": SWAPALPHA, "SWAPalpha": SWAPALPHA, - "BERKELEY": BERKELEY, "MS": MS, "TOFFOLI": TOFFOLI, "FREDKIN": FREDKIN, @@ -81,6 +90,7 @@ "CRX": CRX, "CRY": CRY, "CRZ": CRZ, + "CNOT": CNOT, "CX": CX, "CY": CY, "CZ": CZ, @@ -116,12 +126,16 @@ "QASMU", "SWAP", "ISWAP", + "ISWAPdag", "CNOT", "SQRTSWAP", + "SQRTSWAPdag", "SQRTISWAP", + "SQRTISWAPdag", + "BERKELEY", + "BERKELEYdag", "SWAPALPHA", "MS", - "BERKELEY", "CSIGN", "CRX", "CRY", diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index e82cf0728..547f0c7fc 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -133,7 +133,7 @@ class SQRTSWAP(_TwoQubitGate): Examples -------- >>> from qutip_qip.operations import SQRTSWAP - >>> SQRTSWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> SQRTSWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] @@ -161,6 +161,50 @@ def get_qobj(): dims=[[2, 2], [2, 2]], ) + @staticmethod + def inverse() -> Gate: + return SQRTSWAPdag + + +class SQRTSWAPdag(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{SWAP}}^{\dag}` gate. + + Examples + -------- + >>> from qutip_qip.operations import SQRTSWAPdag + >>> SQRTSWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] + [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse = False + latex_str = r"\sqrt{\rm SWAP}^{\dag}" + + @staticmethod + def get_qobj(): + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], + [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + ) + + @staticmethod + def inverse() -> Gate: + return SQRTSWAP + class SQRTISWAP(_TwoQubitGate): r""" @@ -169,7 +213,7 @@ class SQRTISWAP(_TwoQubitGate): Examples -------- >>> from qutip_qip.operations import SQRTISWAP - >>> SQRTISWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> SQRTISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] @@ -198,6 +242,51 @@ def get_qobj(): dims=[[2, 2], [2, 2]], ) + @staticmethod + def inverse() -> Gate: + return SQRTISWAPdag + + +class SQRTISWAPdag(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{iSWAP}}^{\dag}` gate. + + Examples + -------- + >>> from qutip_qip.operations import SQRTISWAPdag + >>> SQRTISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.70711+0.j 0. -0.70711j 0. +0.j ] + [0. +0.j 0. -0.70711j 0.70711+0.j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse = False + is_clifford = True + latex_str = r"\sqrt{{i}\rm SWAP}^{\dag}" + + @staticmethod + def get_qobj(): + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 1 / np.sqrt(2), -1j / np.sqrt(2), 0], + [0, -1j / np.sqrt(2), 1 / np.sqrt(2), 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + ) + + @staticmethod + def inverse() -> Gate: + return SQRTISWAP + class BERKELEY(_TwoQubitGate): r""" @@ -215,7 +304,7 @@ class BERKELEY(_TwoQubitGate): Examples -------- >>> from qutip_qip.operations import BERKELEY - >>> BERKELEY([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> BERKELEY.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[0.92388+0.j 0. +0.j 0. +0.j 0. +0.38268j] @@ -241,6 +330,57 @@ def get_qobj(): dims=[[2, 2], [2, 2]], ) + @staticmethod + def inverse() -> Gate: + return BERKELEYdag + + +class BERKELEYdag(_TwoQubitGate): + r""" + BERKELEY gate. + + .. math:: + + \begin{pmatrix} + \cos(\frac{\pi}{8}) & 0 & 0 & i\sin(\frac{\pi}{8}) \\ + 0 & \cos(\frac{3\pi}{8}) & i\sin(\frac{3\pi}{8}) & 0 \\ + 0 & i\sin(\frac{3\pi}{8}) & \cos(\frac{3\pi}{8}) & 0 \\ + i\sin(\frac{\pi}{8}) & 0 & 0 & \cos(\frac{\pi}{8}) + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations import BERKELEYdag + >>> BERKELEYdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.92388+0.j 0. +0.j 0. +0.j 0. -0.38268j] + [0. +0.j 0.38268+0.j 0. -0.92388j 0. +0.j ] + [0. +0.j 0. -0.92388j 0.38268+0.j 0. +0.j ] + [0. -0.38268j 0. +0.j 0. +0.j 0.92388+0.j ]] + """ + + __slots__ = () + + self_inverse = False + latex_str = r"{\rm BERKELEY^{\dag}}" + + @staticmethod + def get_qobj(): + return Qobj( + [ + [np.cos(np.pi / 8), 0, 0, -1.0j * np.sin(np.pi / 8)], + [0, np.cos(3 * np.pi / 8), -1.0j * np.sin(3 * np.pi / 8), 0], + [0, -1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], + [-1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], + ], + dims=[[2, 2], [2, 2]], + ) + + @staticmethod + def inverse() -> Gate: + return BERKELEY + class SWAPALPHA(AngleParametricGate): r""" @@ -258,7 +398,7 @@ class SWAPALPHA(AngleParametricGate): Examples -------- >>> from qutip_qip.operations import SWAPALPHA - >>> SWAPALPHA([0, 1], 0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> SWAPALPHA(0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] @@ -295,6 +435,11 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) + def inverse(self) -> Gate: + alpha = self.arg_value[0] + return SWAPALPHA(-alpha) + + class MS(AngleParametricGate): r""" @@ -312,7 +457,7 @@ class MS(AngleParametricGate): Examples -------- >>> from qutip_qip.operations import MS - >>> MS([0, 1], (np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> MS((np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[0.70711+0.j 0. +0.j 0. +0.j 0. -0.70711j] @@ -349,6 +494,10 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) + def inverse(self) -> Gate: + theta, phi = self.arg_value + return MS([-theta, phi]) + class RZX(AngleParametricGate): r""" @@ -395,6 +544,10 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return RZX(-theta) + class CX(_ControlledTwoQubitGate): """ @@ -402,8 +555,8 @@ class CX(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CNOT - >>> CNOT(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations import CX + >>> CX(control_value=1).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0.] @@ -439,7 +592,7 @@ class CY(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations import CY - >>> CY(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> CY().get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[ 1.+0j 0.+0j 0.+0j 0.+0j] @@ -462,7 +615,7 @@ class CZ(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations import CZ - >>> CZ(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> CZ().get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[ 1. 0. 0. 0.] @@ -579,7 +732,7 @@ class CPHASE(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations import CPHASE - >>> CPHASE(0, 1, np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] @@ -643,7 +796,7 @@ class CRZ(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations import CRZ - >>> CRZ(0, 1, np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + >>> CRZ(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] diff --git a/tests/test_gates.py b/tests/test_gates.py index e0657f7f6..36688f03b 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -471,10 +471,16 @@ def test_gates_class(): assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 -GATES = [std.X, std.Y, std.Z, std.H, std.S, std.T, std.SWAP, std.ISWAP] +GATES = [ + std.X, std.Y, std.Z, std.H, std.S, std.Sdag, std.T, std.Tdag, + std.SWAP, std.ISWAP, std.ISWAPdag, std.SQRTSWAP, std.SQRTISWAPdag, + std.SQRTISWAP, std.SQRTISWAPdag, std.BERKELEY, std.BERKELEYdag +] + PARAMETRIC_GATE = [ - std.RX(0.5), std.RY(0.5), std.RZ(0.5), std.PHASE(0.5), - std.R((0.5, 0.9)), std.QASMU((0.1, 0.2, 0.3)) + std.RX(0.5), std.RY(0.5), std.RZ(0.5), std.PHASE(0.5), std.R((0.5, 0.9)), + std.QASMU((0.1, 0.2, 0.3)), std.SWAPALPHA(0.3), std.MS((0.47, 0.8)), + std.RZX(0.6), ] @pytest.mark.parametrize( From 535cb8bd45b6ffbf35a28cf29ecf7de3c992a51c Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 16:01:32 +0530 Subject: [PATCH 020/117] Added checks for unitary_gate method --- src/qutip_qip/algorithms/bit_flip.py | 2 +- src/qutip_qip/algorithms/phase_flip.py | 2 +- src/qutip_qip/algorithms/qft.py | 3 ++- src/qutip_qip/algorithms/qpe.py | 5 +++-- src/qutip_qip/circuit/_decompose.py | 4 +++- src/qutip_qip/circuit/circuit.py | 18 +++++------------- src/qutip_qip/compiler/cavityqedcompiler.py | 2 +- src/qutip_qip/compiler/circuitqedcompiler.py | 4 ++-- src/qutip_qip/compiler/scheduler.py | 2 +- .../decompose/decompose_single_qubit_gate.py | 3 +-- src/qutip_qip/device/circuitqed.py | 2 +- src/qutip_qip/device/optpulseprocessor.py | 4 ++-- src/qutip_qip/operations/gateclass.py | 18 +++++++----------- 13 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/qutip_qip/algorithms/bit_flip.py b/src/qutip_qip/algorithms/bit_flip.py index d1a1665fc..18b3551cd 100644 --- a/src/qutip_qip/algorithms/bit_flip.py +++ b/src/qutip_qip/algorithms/bit_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import CX, TOFFOLI, X +from qutip_qip.operations.std import CX, TOFFOLI, X class BitFlipCode: diff --git a/src/qutip_qip/algorithms/phase_flip.py b/src/qutip_qip/algorithms/phase_flip.py index 2441b57cd..6839f9a42 100644 --- a/src/qutip_qip/algorithms/phase_flip.py +++ b/src/qutip_qip/algorithms/phase_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import CX, H +from qutip_qip.operations.std import CX, H class PhaseFlipCode: diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index 666d845e0..c64823b27 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -3,7 +3,8 @@ """ import numpy as np -from qutip_qip.operations import H, RZ, CX, CPHASE, SWAP, expand_operator +from qutip_qip.operations import expand_operator +from qutip_qip.operations.std import H, RZ, CX, CPHASE, SWAP from qutip_qip.circuit import QubitCircuit from qutip import Qobj from qutip_qip.decompose import decompose_one_qubit_gate diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 9d43cd64a..7020144c5 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,8 @@ import numpy as np -from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence -from qutip_qip.operations import unitary_gate, controlled_gate, H, Gate +from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations import unitary_gate, controlled_gate, Gate +from qutip_qip.operations.std import H def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 36aa3c534..4fb8eb5f0 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -5,7 +5,9 @@ """ import numpy as np -from qutip_qip.operations import RX, RY, RZ, CX, ISWAP, SQRTSWAP, SQRTISWAP, CZ +from qutip_qip.operations.std import ( + RX, RY, RZ, CX, ISWAP, SQRTSWAP, SQRTISWAP, CZ +) __all__ = ["_resolve_to_universal", "_resolve_2q_basis"] diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 1e42de213..5cf097076 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -2,22 +2,12 @@ Quantum circuit representation and simulation. """ -import numpy as np import warnings from typing import Iterable -from qutip_qip.typing import IntList +from qutip import qeye, Qobj +import numpy as np from ._decompose import _resolve_to_universal, _resolve_2q_basis -from qutip_qip.operations import ( - Gate, - GLOBALPHASE, - RX, - RY, - RZ, - Measurement, - expand_operator, - GATE_CLASS_MAP, -) from qutip_qip.circuit import ( CircuitSimulator, CircuitInstruction, @@ -25,7 +15,9 @@ MeasurementInstruction, ) from qutip_qip.circuit.utils import _check_iterable, _check_limit_ -from qutip import qeye, Qobj +from qutip_qip.operations import Gate, Measurement, expand_operator +from qutip_qip.operations.std import RX, RY,RZ, GLOBALPHASE, GATE_CLASS_MAP +from qutip_qip.typing import IntList try: from IPython.display import Image as DisplayImage, SVG as DisplaySVG diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index d4ea3d9d8..26673eaa4 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -1,8 +1,8 @@ import numpy as np from qutip_qip.circuit import GateInstruction -from qutip_qip.operations import RZ from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.std import RZ class CavityQEDCompiler(GateCompiler): diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 2780af9a4..affc70934 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -1,8 +1,8 @@ import numpy as np from qutip_qip.circuit import GateInstruction -from qutip_qip.operations import RX, RY, RZX from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.std import RX, RY, RZX class SCQubitsCompiler(GateCompiler): @@ -69,7 +69,7 @@ class SCQubitsCompiler(GateCompiler): >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import ModelProcessor, SCQubitsModel >>> from qutip_qip.compiler import SCQubitsCompiler - >>> from qutip_qip.operations import CX + >>> from qutip_qip.operations.std import CX >>> >>> qc = QubitCircuit(2) >>> qc.add_gate(CX, targets=0, controls=1) diff --git a/src/qutip_qip/compiler/scheduler.py b/src/qutip_qip/compiler/scheduler.py index 2398303bc..649c1f821 100644 --- a/src/qutip_qip/compiler/scheduler.py +++ b/src/qutip_qip/compiler/scheduler.py @@ -426,7 +426,7 @@ def schedule( -------- >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.compiler import Scheduler - >>> from qutip_qip.operations import H, CZ, SWAP + >>> from qutip_qip.operations.std import H, CZ, SWAP >>> circuit = QubitCircuit(7) >>> circuit.add_gate(H, targets=3) # gate0 >>> circuit.add_gate(CZ, targets=5, controls=3) # gate1 diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index 726add861..f1d9e9ebb 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -3,8 +3,7 @@ import cmath from qutip_qip.decompose._utility import check_gate - -from qutip_qip.operations import GLOBALPHASE, X, RX, RY, RZ +from qutip_qip.operations.std import GLOBALPHASE, X, RX, RY, RZ class MethodError(Exception): diff --git a/src/qutip_qip/device/circuitqed.py b/src/qutip_qip/device/circuitqed.py index d6dafdffa..01b27d786 100644 --- a/src/qutip_qip/device/circuitqed.py +++ b/src/qutip_qip/device/circuitqed.py @@ -46,7 +46,7 @@ class SCQubits(ModelProcessor): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import SCQubits - from qutip_qip.operations import RY, RZ, CX + from qutip_qip.operations.std import RY, RZ, CX qc = QubitCircuit(2) qc.add_gate(RZ, targets=0, arg_value=np.pi) diff --git a/src/qutip_qip/device/optpulseprocessor.py b/src/qutip_qip/device/optpulseprocessor.py index e2bfe1385..f005099bf 100644 --- a/src/qutip_qip/device/optpulseprocessor.py +++ b/src/qutip_qip/device/optpulseprocessor.py @@ -70,7 +70,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations import H + >>> from qutip_qip.operations.std import H >>> qc = QubitCircuit(1) >>> qc.add_gate(H, targets=0) >>> num_tslots = 10 @@ -85,7 +85,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations import H, SWAP, CX + >>> from qutip_qip.operations.std import H, SWAP, CX >>> qc = QubitCircuit(2) >>> qc.add_gate(H, targets=0) >>> qc.add_gate(SWAP, targets=[0, 1]) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 394e8914d..41d1c280b 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -258,30 +258,26 @@ def unitary_gate(gate_name: str, U: Qobj, namespace: str = "custom") -> Gate: Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ + # Check whether U is unitary n = np.log2(U.shape[0]) - inverse = U == U.dag() - if n != np.log2(U.shape[1]): - raise ValueError("The unitary U must be square.") + 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__ = () - name = gate_name _namespace = namespace + name = gate_name num_qubits = int(n) - self_inverse = inverse + self_inverse = (U == U.dag()) @staticmethod def get_qobj(): return U return _CustomGate - -def inverse_gate(gate_name: str, gate: Gate, namespace: str): - class _InverseGate(Gate): - pass - - return _InverseGate \ No newline at end of file From 06d8ca44894fac842d6a7af5c21042b64b8dfa86 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 16:13:43 +0530 Subject: [PATCH 021/117] Use qutip_qip.operations.std import for std gates import --- src/qutip_qip/device/processor.py | 2 +- src/qutip_qip/operations/__init__.py | 7 ++- src/qutip_qip/operations/std/other_gates.py | 10 +-- .../operations/std/single_qubit_gate.py | 34 +++++----- .../operations/std/two_qubit_gate.py | 46 +++++++------- src/qutip_qip/operations/utils.py | 3 +- src/qutip_qip/qiskit/utils/converter.py | 62 +++++++------------ src/qutip_qip/transpiler/chain.py | 2 +- .../test_single_qubit_gate_decompositions.py | 2 +- tests/test_bit_flip.py | 2 +- tests/test_compiler.py | 3 +- tests/test_device.py | 4 +- tests/test_noise.py | 2 +- tests/test_optpulseprocessor.py | 2 +- tests/test_phase_flip.py | 2 +- tests/test_processor.py | 7 ++- tests/test_qiskit.py | 2 +- tests/test_qpe.py | 4 +- tests/test_renderer.py | 4 +- tests/test_scheduler.py | 4 +- tests/test_vqa.py | 3 +- 21 files changed, 97 insertions(+), 110 deletions(-) diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 9c235b054..013add8e0 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -4,7 +4,7 @@ import numpy as np from qutip import Qobj, QobjEvo, mesolve, mcsolve -from qutip_qip.operations import GLOBALPHASE +from qutip_qip.operations.std import GLOBALPHASE from qutip_qip.noise import Noise, process_noise from qutip_qip.device import Model from qutip_qip.device.utils import _pulse_interpolate diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 623190222..88cdaa3cc 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -47,6 +47,7 @@ qubit_clifford_group, ) +# This should ideally be removed as it leads to duplication from .std import ( X, Y, @@ -102,8 +103,6 @@ "controlled_gate", "AngleParametricGate", "Measurement", - "GATE_CLASS_MAP", - "GLOBALPHASE", "rx", "ry", "rz", @@ -140,6 +139,10 @@ "qubit_clifford_group", "expand_operator", "gate_sequence_product", + + # The below ones need to be removed (should only be maintained in ./std), no duplication + "GATE_CLASS_MAP", + "GLOBALPHASE", "X", "Y", "Z", diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/std/other_gates.py index 827e5bb2e..27c2c46af 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/std/other_gates.py @@ -12,7 +12,7 @@ class GLOBALPHASE(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations import GLOBALPHASE + >>> from qutip_qip.operations.std import GLOBALPHASE """ num_qubits: int = 0 @@ -45,8 +45,8 @@ class TOFFOLI(ControlledGate): Examples -------- - >>> from qutip_qip.operations import TOFFOLI - >>> TOFFOLI([0, 1, 2]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations.std import TOFFOLI + >>> TOFFOLI.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -73,8 +73,8 @@ class FREDKIN(ControlledGate): Examples -------- - >>> from qutip_qip.operations import FREDKIN - >>> FREDKIN([0, 1, 2]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations.std import FREDKIN + >>> FREDKIN.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 34c57b875..df7726c1e 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -25,7 +25,7 @@ class X(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import X + >>> from qutip_qip.operations.std import X >>> X.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -50,7 +50,7 @@ class Y(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import Y + >>> from qutip_qip.operations.std import Y >>> Y.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -75,7 +75,7 @@ class Z(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import Z + >>> from qutip_qip.operations.std import Z >>> Z.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -100,7 +100,7 @@ class IDLE(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import IDLE + >>> from qutip_qip.operations.std import IDLE """ __slots__ = () @@ -120,7 +120,7 @@ class H(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import H + >>> from qutip_qip.operations.std import H >>> H.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -161,7 +161,7 @@ class SQRTX(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTX + >>> from qutip_qip.operations.std import SQRTX >>> SQRTX.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -190,7 +190,7 @@ class SQRTXdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTXdag + >>> from qutip_qip.operations.std import SQRTXdag >>> SQRTXdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -232,7 +232,7 @@ class S(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import S + >>> from qutip_qip.operations.std import S >>> S.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -261,7 +261,7 @@ class Sdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import S + >>> from qutip_qip.operations.std import S >>> Sdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -290,7 +290,7 @@ class T(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import T + >>> from qutip_qip.operations.std import T >>> T.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -318,7 +318,7 @@ class Tdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations import Tdag + >>> from qutip_qip.operations.std import Tdag >>> Tdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -346,7 +346,7 @@ class RX(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import RX + >>> from qutip_qip.operations.std import RX >>> RX(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -380,7 +380,7 @@ class RY(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import RY + >>> from qutip_qip.operations.std import RY >>> RY(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -413,7 +413,7 @@ class RZ(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import RZ + >>> from qutip_qip.operations.std import RZ >>> RZ(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -441,7 +441,7 @@ class PHASE(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import PHASE + >>> from qutip_qip.operations.std import PHASE """ __slots__ = () @@ -476,7 +476,7 @@ class R(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import R + >>> from qutip_qip.operations.std import R >>> R((np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -518,7 +518,7 @@ class QASMU(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations import QASMU + >>> from qutip_qip.operations.std import QASMU >>> QASMU(0, (np.pi/2, np.pi, np.pi/2)).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 547f0c7fc..e4e67ff1d 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -34,7 +34,7 @@ class SWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import SWAP + >>> from qutip_qip.operations.std import SWAP >>> SWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -64,7 +64,7 @@ class ISWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import ISWAP + >>> from qutip_qip.operations.std import ISWAP >>> ISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -98,7 +98,7 @@ class ISWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import ISWAPdag + >>> from qutip_qip.operations.std import ISWAPdag >>> ISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -132,7 +132,7 @@ class SQRTSWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTSWAP + >>> from qutip_qip.operations.std import SQRTSWAP >>> SQRTSWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -172,7 +172,7 @@ class SQRTSWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTSWAPdag + >>> from qutip_qip.operations.std import SQRTSWAPdag >>> SQRTSWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -212,7 +212,7 @@ class SQRTISWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTISWAP + >>> from qutip_qip.operations.std import SQRTISWAP >>> SQRTISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -253,7 +253,7 @@ class SQRTISWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import SQRTISWAPdag + >>> from qutip_qip.operations.std import SQRTISWAPdag >>> SQRTISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -303,7 +303,7 @@ class BERKELEY(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import BERKELEY + >>> from qutip_qip.operations.std import BERKELEY >>> BERKELEY.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -350,7 +350,7 @@ class BERKELEYdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations import BERKELEYdag + >>> from qutip_qip.operations.std import BERKELEYdag >>> BERKELEYdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -397,7 +397,7 @@ class SWAPALPHA(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations import SWAPALPHA + >>> from qutip_qip.operations.std import SWAPALPHA >>> SWAPALPHA(0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -456,7 +456,7 @@ class MS(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations import MS + >>> from qutip_qip.operations.std import MS >>> MS((np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -514,7 +514,7 @@ class RZX(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations import RZX + >>> from qutip_qip.operations.std import RZX >>> RZX(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -555,7 +555,7 @@ class CX(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CX + >>> from qutip_qip.operations.std import CX >>> CX(control_value=1).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -591,7 +591,7 @@ class CY(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CY + >>> from qutip_qip.operations.std import CY >>> CY().get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -614,7 +614,7 @@ class CZ(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CZ + >>> from qutip_qip.operations.std import CZ >>> CZ().get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -659,7 +659,7 @@ class CH(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CH + >>> from qutip_qip.operations.std import CH """ __slots__ = () @@ -683,7 +683,7 @@ class CT(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CPHASE + >>> from qutip_qip.operations.std import CT """ __slots__ = () @@ -707,7 +707,7 @@ class CS(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CPHASE + >>> from qutip_qip.operations.std import CS """ __slots__ = () @@ -731,7 +731,7 @@ class CPHASE(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CPHASE + >>> from qutip_qip.operations.std import CPHASE >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -754,7 +754,7 @@ class CRX(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CRX + >>> from qutip_qip.operations.std import CRX """ __slots__ = () @@ -770,7 +770,7 @@ class CRY(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CRY + >>> from qutip_qip.operations.std import CRY """ __slots__ = () @@ -795,7 +795,7 @@ class CRZ(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CRZ + >>> from qutip_qip.operations.std import CRZ >>> CRZ(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -818,7 +818,7 @@ class CQASMU(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations import CQASMU + >>> from qutip_qip.operations.std import CQASMU """ __slots__ = () diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index e6ddd4a2d..3f138a4a1 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -121,7 +121,8 @@ def expand_operator( Examples -------- - >>> from qutip_qip.operations import expand_operator, X, CNOT + >>> from qutip_qip.operations import expand_operator + >>> from qutip_qip.operations.std import X, CNOT >>> import qutip >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index f4346f837..faf8ae773 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -2,53 +2,33 @@ from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ( - X, - Y, - Z, - H, - S, - T, - RX, - RY, - RZ, - SWAP, - QASMU, - PHASE, - CX, - CY, - CZ, - CPHASE, - CRX, - CRY, - CRZ, - Gate, -) +from qutip_qip.operations import Gate +import qutip_qip.operations.std as std # TODO Expand this dictionary for other gates like CS etc. _map_gates: dict[str, Gate] = { - "p": PHASE, - "x": X, - "y": Y, - "z": Z, - "h": H, - "s": S, - "t": T, - "rx": RX, - "ry": RY, - "rz": RZ, - "swap": SWAP, - "u": QASMU, + "p": std.PHASE, + "x": std.X, + "y": std.Y, + "z": std.Z, + "h": std.H, + "s": std.S, + "t": std.T, + "rx": std.RX, + "ry": std.RY, + "rz": std.RZ, + "swap": std.SWAP, + "u": std.QASMU, } _map_controlled_gates: dict[str, Gate] = { - "cx": CX, - "cy": CY, - "cz": CZ, - "crx": CRX, - "cry": CRY, - "crz": CRZ, - "cp": CPHASE, + "cx": std.CX, + "cy": std.CY, + "cz": std.CZ, + "crx": std.CRX, + "cry": std.CRY, + "crz": std.CRZ, + "cp": std.CPHASE, } _ignore_gates: list[str] = ["id", "barrier"] diff --git a/src/qutip_qip/transpiler/chain.py b/src/qutip_qip/transpiler/chain.py index 558a2500e..ecae982ee 100644 --- a/src/qutip_qip/transpiler/chain.py +++ b/src/qutip_qip/transpiler/chain.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import SWAP, RX +from qutip_qip.operations.std import SWAP def to_chain_structure(qc: QubitCircuit, setup="linear"): diff --git a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py index ca53852a9..f5733ec66 100644 --- a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py +++ b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py @@ -9,7 +9,7 @@ ) from qutip_qip.decompose import decompose_one_qubit_gate from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import H, X, Y, Z, S, T, SQRTX +from qutip_qip.operations.std import H, X, Y, Z, S, T, SQRTX # Fidelity closer to 1 means the two states are similar to each other target = 0 diff --git a/tests/test_bit_flip.py b/tests/test_bit_flip.py index 0ad163c33..e8ca5ce74 100644 --- a/tests/test_bit_flip.py +++ b/tests/test_bit_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import BitFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import X +from qutip_qip.operations.std import X @pytest.fixture diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 77aa64f3c..7d8cb17d5 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,8 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ParametricGate, X, RX +from qutip_qip.operations import ParametricGate +from qutip_qip.operations.std import X, RX from qutip import basis, fidelity diff --git a/tests/test_device.py b/tests/test_device.py index 37ebb43ba..607f54f2b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -9,7 +9,8 @@ import qutip from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ( +from qutip_qip.operations import gate_sequence_product +from qutip_qip.operations.std import ( X, Y, Z, @@ -22,7 +23,6 @@ ISWAP, SQRTISWAP, RZX, - gate_sequence_product, ) from qutip_qip.device import ( DispersiveCavityQED, diff --git a/tests/test_noise.py b/tests/test_noise.py index cc20c4d87..cf53b6211 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -16,7 +16,7 @@ sigmam, ) from qutip_qip.device import Processor, SCQubits, LinearSpinChain -from qutip_qip.operations import X +from qutip_qip.operations.std import X from qutip_qip.noise import ( RelaxationNoise, DecoherenceNoise, diff --git a/tests/test_optpulseprocessor.py b/tests/test_optpulseprocessor.py index 0597cbf91..486bae91b 100644 --- a/tests/test_optpulseprocessor.py +++ b/tests/test_optpulseprocessor.py @@ -15,7 +15,7 @@ sigmay, identity, ) -from qutip_qip.operations import X, CX, H, SWAP +from qutip_qip.operations.std import X, CX, H, SWAP class TestOptPulseProcessor: diff --git a/tests/test_phase_flip.py b/tests/test_phase_flip.py index 990a6bfd2..50428034f 100644 --- a/tests/test_phase_flip.py +++ b/tests/test_phase_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import PhaseFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import Z +from qutip_qip.operations.std import Z @pytest.fixture diff --git a/tests/test_processor.py b/tests/test_processor.py index 3b2a93870..47ee32263 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -5,7 +5,6 @@ import pytest import qutip -from qutip_qip.device import Processor, LinearSpinChain from qutip import ( basis, sigmaz, @@ -18,10 +17,12 @@ fidelity, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import hadamard_transform, ISWAP, X +from qutip_qip.device import Processor, LinearSpinChain +from qutip_qip.operations import hadamard_transform +from qutip_qip.operations.std import ISWAP, X from qutip_qip.noise import DecoherenceNoise, RandomNoise, ControlAmpNoise -from qutip_qip.qubits import qubit_states from qutip_qip.pulse import Pulse +from qutip_qip.qubits import qubit_states class TestCircuitProcessor: diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 51e5237ed..d30df6bc4 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -15,7 +15,7 @@ CircularSpinChain, DispersiveCavityQED, ) -from qutip_qip.operations import X, CX, RX, H +from qutip_qip.operations.std import X, CX, RX, H from qiskit import QuantumCircuit from qiskit_aer import AerSimulator diff --git a/tests/test_qpe.py b/tests/test_qpe.py index b23254e5c..9c66cffdf 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -1,10 +1,10 @@ +import unittest import numpy as np from numpy.testing import assert_, assert_equal -import unittest from qutip import Qobj, sigmaz, tensor -from qutip_qip.operations import controlled_gate, unitary_gate from qutip_qip.algorithms.qpe import qpe +from qutip_qip.operations import controlled_gate, unitary_gate class TestQPE(unittest.TestCase): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index e610aa3d6..50aea779b 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -3,8 +3,8 @@ from unittest.mock import patch from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer -from qutip_qip.operations import ( - controlled_gate, +from qutip_qip.operations import controlled_gate +from qutip_qip.operations.std import ( IDLE, X, H, diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index deb7659d2..948b0b1c7 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -3,9 +3,9 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.compiler import PulseInstruction, Scheduler -from qutip_qip.operations import ( +from qutip_qip.operations import ControlledGate +from qutip_qip.operations.std import ( GATE_CLASS_MAP, - ControlledGate, CX, X, Z, diff --git a/tests/test_vqa.py b/tests/test_vqa.py index 394855fac..b57d98f3d 100644 --- a/tests/test_vqa.py +++ b/tests/test_vqa.py @@ -1,7 +1,8 @@ import pytest import numpy as np import qutip -from qutip_qip.operations import expand_operator, H +from qutip_qip.operations import expand_operator +from qutip_qip.operations.std import H from qutip_qip.vqa import ( VQA, VQABlock, From 2fdde0284b03b55d2d29f8ad757b554332b7a6dd Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 19:39:35 +0530 Subject: [PATCH 022/117] Added inverse method for controlled gates --- src/qutip_qip/operations/std/two_qubit_gate.py | 4 +++- tests/test_gates.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index e4e67ff1d..6aa00be1b 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -440,7 +440,6 @@ def inverse(self) -> Gate: return SWAPALPHA(-alpha) - class MS(AngleParametricGate): r""" Mølmer–Sørensen gate. @@ -667,6 +666,9 @@ class CH(_ControlledTwoQubitGate): target_gate = H latex_str = r"{\rm CH}" + def inverse(self): + return CH(self.control_value) + class CT(_ControlledTwoQubitGate): r""" diff --git a/tests/test_gates.py b/tests/test_gates.py index 36688f03b..f956516bf 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -424,7 +424,7 @@ def test_gates_class(): std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0 ) circuit1.add_gate(std.CX, controls=0, targets=1) - circuit1.add_gate(std.CPHASE(np.pi / 4), controls=0, targets=1) + circuit1.add_gate(std.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) circuit1.add_gate(std.SWAP, targets=[0, 1]) circuit1.add_gate(std.ISWAP, targets=[2, 1]) circuit1.add_gate(std.CZ, controls=[0], targets=[2]) @@ -455,7 +455,7 @@ def test_gates_class(): targets=0, ) circuit2.add_gate(std.CX, controls=0, targets=1) - circuit2.add_gate(std.CPHASE(np.pi / 4), controls=0, targets=1) + circuit2.add_gate(std.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) circuit2.add_gate(std.SWAP, targets=[0, 1]) circuit2.add_gate(std.ISWAP, targets=[2, 1]) circuit2.add_gate(std.CZ, controls=[0], targets=[2]) @@ -483,8 +483,14 @@ def test_gates_class(): std.RZX(0.6), ] +CONTROLLED_GATE = [ + std.CX(), std.CY(), std.CZ(), std.CH(), std.CS(), std.CT(), + std.CRX(arg_value=0.7), std.CRY(arg_value=0.88), std.CRZ(arg_value=0.78), + std.CPHASE(arg_value=0.9), std.CQASMU(arg_value=[0.9, 0.22, 0.15]) +] + @pytest.mark.parametrize( - "gate", GATES + PARAMETRIC_GATE + "gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE ) def test_gate_inverse(gate: Gate): n = 2**gate.num_qubits From 8f20aeb0e47cdb7a28e9939457d02f71eb8244cb Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 19:43:49 +0530 Subject: [PATCH 023/117] Minor change to argument ordering for ControlledGate --- src/qutip_qip/algorithms/qft.py | 10 ++++----- src/qutip_qip/operations/controlled.py | 28 +++++++++++++++++++++--- src/qutip_qip/operations/gateclass.py | 3 ++- src/qutip_qip/operations/std/__init__.py | 22 +++++++++++++++---- src/qutip_qip/qasm.py | 2 +- tests/test_circuit.py | 4 ++-- tests/test_qasm.py | 8 +++---- tests/test_renderer.py | 10 ++++----- 8 files changed, 62 insertions(+), 25 deletions(-) diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index c64823b27..4d647d52c 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -3,11 +3,11 @@ """ import numpy as np -from qutip_qip.operations import expand_operator -from qutip_qip.operations.std import H, RZ, CX, CPHASE, SWAP -from qutip_qip.circuit import QubitCircuit from qutip import Qobj +from qutip_qip.circuit import QubitCircuit from qutip_qip.decompose import decompose_one_qubit_gate +from qutip_qip.operations import expand_operator +from qutip_qip.operations.std import H, RZ, CX, CPHASE, SWAP def qft(N=1): @@ -67,7 +67,7 @@ def qft_steps(N=1, swapping=True): for j in range(i): U_step_list.append( expand_operator( - CPHASE(np.pi / (2 ** (i - j))).get_qobj(), + CPHASE(arg_value=np.pi / (2 ** (i - j))).get_qobj(), dims=[2] * N, targets=[i, j], ) @@ -115,7 +115,7 @@ def qft_gate_sequence(N=1, swapping=True, to_cnot=False): if not to_cnot: qc.add_gate( CPHASE( - np.pi / (2 ** (i - j)), + arg_value=np.pi / (2 ** (i - j)), arg_label=r"{\pi/2^{%d}}" % (i - j), ), targets=[j], diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 7e829fbcb..55f781a88 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -79,6 +79,7 @@ def __init_subclass__(cls, **kwargs): # Automatically copy the validator from the target if hasattr(cls.target_gate, "validate_params"): cls.validate_params = staticmethod(cls.target_gate.validate_params) + cls.num_params = cls.target_gate.num_params # Default set_inverse cls.self_inverse = cls.target_gate.self_inverse @@ -89,8 +90,8 @@ def __init_subclass__(cls, **kwargs): def __init__( self, - arg_value: any = None, control_value: int | None = None, + arg_value: any = None, arg_label: str | None = None, ) -> None: if control_value is not None: @@ -161,6 +162,24 @@ def get_qobj(self) -> Qobj: control_value=self.control_value, ) + def inverse(self) -> Gate: + if not self.is_parametric_gate(): + inverse_gate = controlled_gate( + self.target_gate.inverse(), self.num_ctrl_qubits + ) + return inverse_gate(control_value=self.control_value) + + else: + inverse_target_gate = self.target_gate(self.arg_value).inverse() + arg_value = inverse_target_gate.arg_value + inverse_gate = controlled_gate( + type(inverse_target_gate), self.num_ctrl_qubits + ) + + return inverse_gate( + control_value = self.control_value, arg_value=arg_value + ) + @staticmethod def is_controlled_gate() -> bool: return True @@ -184,13 +203,16 @@ def __eq__(self, other) -> bool: def controlled_gate( gate: Gate, n_ctrl_qubits: int = 1, - gate_name: str = "_CustomControlledGate", + gate_name: str | None = None, namespace: str = "custom", ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ + if gate_name is None: + gate_name = f"C{gate.name}" + class _CustomControlledGate(ControlledGate): __slots__ = () _namespace = namespace @@ -198,6 +220,6 @@ class _CustomControlledGate(ControlledGate): num_qubits = n_ctrl_qubits + gate.num_qubits num_ctrl_qubits = n_ctrl_qubits target_gate = gate - latex_str = rf"C{gate.name}" + latex_str = r"{gate_name}" return _CustomControlledGate diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 41d1c280b..b16e3eba4 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -13,7 +13,8 @@ class _GateMetaClass(ABCMeta): ( "num_qubits", "num_ctrl_qubits", - "num_params", + # "num_params", + # "self_inverse", "target_gate", "is_clifford", ) diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/std/__init__.py index 70957fc11..61d61f518 100644 --- a/src/qutip_qip/operations/std/__init__.py +++ b/src/qutip_qip/operations/std/__init__.py @@ -32,16 +32,16 @@ SWAPALPHA, MS, RZX, + CNOT, CX, CY, CZ, - CRX, - CRY, - CRZ, CS, CT, CH, - CNOT, + CRX, + CRY, + CRZ, CPHASE, CSIGN, CQASMU, @@ -102,6 +102,20 @@ "CQASMU": CQASMU, } +CONTROLLED_GATE_MAP = { + X: CX, + Y: CY, + Z: CZ, + H: CH, + S: CS, + T: CT, + RX: CRX, + RY: CRY, + RZ: CRZ, + PHASE: CPHASE, + QASMU: CQASMU, +} + __all__ = [ "GATE_CLASS_MAP", diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 6dd1053a9..2b77bad31 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -651,7 +651,7 @@ def _add_qiskit_gates( ) elif name == "cu3": qc.add_gate( - std.CQASMU(args), + std.CQASMU(arg_value=args), controls=regs[0], targets=[regs[1]], classical_controls=classical_controls, diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 6068d584f..e0a6f1ca1 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -209,7 +209,7 @@ def test_add_circuit(self): qc.add_measurement("M0", targets=[0], classical_store=[1]) qc.add_gate(std.RY(1.570796), targets=4) qc.add_gate(std.RY(1.570796), targets=5) - qc.add_gate(std.CRX(np.pi / 2), controls=[1], targets=[2]) + qc.add_gate(std.CRX(arg_value=np.pi / 2), controls=[1], targets=[2]) qc1 = QubitCircuit(6) qc1.add_circuit(qc) @@ -426,7 +426,7 @@ def get_qobj(): return Qobj(mat, dims=[[2], [2]]) qc = QubitCircuit(3) - qc.add_gate(std.CRX(np.pi / 2), targets=[2], controls=[1]) + qc.add_gate(std.CRX(arg_value=np.pi / 2), targets=[2], controls=[1]) qc.add_gate(T1, targets=[1]) props = qc.propagators() result1 = tensor(identity(2), customer_gate1(np.pi / 2)) diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 13a64bfee..52641aac6 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -137,10 +137,10 @@ def test_qasm_str(): def test_export_import(): qc = QubitCircuit(3) - qc.add_gate(std.CRY(np.pi), targets=1, controls=0) - qc.add_gate(std.CRX(np.pi), targets=1, controls=0) - qc.add_gate(std.CRZ(np.pi), targets=1, controls=0) - qc.add_gate(std.CX, targets=1, controls=0) + qc.add_gate(std.CRY(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(std.CRX(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(std.CRZ(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(std.CX(), targets=1, controls=0) qc.add_gate(std.TOFFOLI, targets=2, controls=[0, 1]) # qc.add_gate(SQRTX, targets=0) qc.add_gate(std.CS, targets=1, controls=0) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 50aea779b..fd80ccaa0 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -23,13 +23,13 @@ def qc1(): qc = QubitCircuit(4) qc.add_gate(ISWAP, targets=[2, 3]) - qc.add_gate(CRX(np.pi / 2), targets=[0], controls=[1]) + qc.add_gate(CRX(arg_value=np.pi / 2), targets=[0], controls=[1]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(TOFFOLI, controls=[0, 2], targets=[1]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(0.5), controls=[2], targets=[3]) + qc.add_gate(CRX(arg_value=0.5), controls=[2], targets=[3]) qc.add_gate(SWAP, targets=[0, 3]) return qc @@ -71,7 +71,7 @@ def qc2(): qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(0.5), controls=[0], targets=[1]) + qc.add_gate(CRX(arg_value=0.5), controls=[0], targets=[1]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_measurement("M", targets=[0], classical_store=0) @@ -152,11 +152,11 @@ def qc3(): qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(0.5), controls=[0], targets=[1]) + qc.add_gate(CRX(arg_value=0.5), controls=[0], targets=[1]) qc.add_measurement("M", targets=[0], classical_store=0) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(CPHASE(0.75), controls=[2], targets=[3]) + qc.add_gate(CPHASE(arg_value=0.75), controls=[2], targets=[3]) qc.add_gate(ISWAP, targets=[1, 3]) qc.add_measurement("M", targets=[1], classical_store=1) return qc From fd4214ca8512afd5f69c4dd18078ae367b112d9f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 23 Feb 2026 19:48:53 +0530 Subject: [PATCH 024/117] Added inverse gate test for TOFFOLI, FREDKIN --- src/qutip_qip/operations/gateclass.py | 2 +- tests/test_gates.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index b16e3eba4..5e68ce079 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -95,7 +95,7 @@ class X(Gate): for attribute in cls._read_only_set: if name == attribute and hasattr(cls, attribute): raise AttributeError(f"{attribute} is read-only!") - super().__setattr__(name, value) + super().__setattr__(name, value) class Gate(ABC, metaclass=_GateMetaClass): diff --git a/tests/test_gates.py b/tests/test_gates.py index f956516bf..efc0fce15 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -486,7 +486,8 @@ def test_gates_class(): CONTROLLED_GATE = [ std.CX(), std.CY(), std.CZ(), std.CH(), std.CS(), std.CT(), std.CRX(arg_value=0.7), std.CRY(arg_value=0.88), std.CRZ(arg_value=0.78), - std.CPHASE(arg_value=0.9), std.CQASMU(arg_value=[0.9, 0.22, 0.15]) + std.CPHASE(arg_value=0.9), std.CQASMU(arg_value=[0.9, 0.22, 0.15]), + std.TOFFOLI(), std.FREDKIN() ] @pytest.mark.parametrize( @@ -499,4 +500,4 @@ def test_gate_inverse(gate: Gate): (gate.get_qobj() * inverse_gate.get_qobj()).full(), np.eye(n), atol=1e-12 - ) \ No newline at end of file + ) From e9aa797c6908f91b25d603600f3a0331a3a3cd7c Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 24 Feb 2026 00:18:54 +0530 Subject: [PATCH 025/117] Corrected __setattr__ for Gate metaclass --- src/qutip_qip/operations/controlled.py | 15 ++++++++++----- src/qutip_qip/operations/gateclass.py | 15 +++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 55f781a88..a8fb0e078 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -77,16 +77,21 @@ def __init_subclass__(cls, **kwargs): ) # Automatically copy the validator from the target - if hasattr(cls.target_gate, "validate_params"): + if cls.target_gate.is_parametric_gate(): cls.validate_params = staticmethod(cls.target_gate.validate_params) - cls.num_params = cls.target_gate.num_params - # Default set_inverse - cls.self_inverse = cls.target_gate.self_inverse + # Copy the num_params if not defined + if "num_params" not in cls.__dict__: + cls.num_params = cls.target_gate.num_params + + # Default self_inverse + 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. - cls.latex_str = cls.target_gate.latex_str + if "latex_str" not in cls.__dict__: + cls.latex_str = cls.target_gate.latex_str def __init__( self, diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 5e68ce079..e1a381ff3 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -13,10 +13,11 @@ class _GateMetaClass(ABCMeta): ( "num_qubits", "num_ctrl_qubits", - # "num_params", - # "self_inverse", - "target_gate", + "num_params", + "self_inverse", "is_clifford", + "target_gate", + "latex_str", ) ) @@ -92,9 +93,11 @@ class X(Gate): This is required since num_qubits etc. are class attributes (shared by all object instances). """ - for attribute in cls._read_only_set: - if name == attribute and hasattr(cls, attribute): - raise AttributeError(f"{attribute} is read-only!") + # Check if the attribute is in our protected set + # Using cls.__dict__ ignores parent classes, allowing __init_subclass__ + # to set it once for the child class. + if name in cls._read_only_set and name in cls.__dict__: + raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) From f5aa4210db3c73e20c0ee86c311e43121f916754 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 24 Feb 2026 00:25:29 +0530 Subject: [PATCH 026/117] Rename is_controlled_gate method to is_controlled --- src/qutip_qip/circuit/circuit.py | 4 ++-- src/qutip_qip/circuit/draw/text_renderer.py | 4 ++-- src/qutip_qip/circuit/instruction.py | 4 ++-- src/qutip_qip/compiler/instruction.py | 2 +- src/qutip_qip/operations/controlled.py | 2 +- src/qutip_qip/operations/gateclass.py | 2 +- tests/test_circuit.py | 4 ++-- tests/test_qiskit.py | 4 ++-- tests/test_qpe.py | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 5cf097076..6c6a49eef 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -356,7 +356,7 @@ def add_gate( ) if ( - gate_class.is_controlled_gate() + gate_class.is_controlled() and gate_class.is_parametric_gate() ): gate = gate_class( @@ -368,7 +368,7 @@ def add_gate( elif gate_class.is_parametric_gate(): gate = gate_class(arg_value=arg_value, arg_label=arg_label) - elif gate_class.is_controlled_gate(): + elif gate_class.is_controlled(): gate = gate_class(control_value=control_value) else: gate = gate_class diff --git a/src/qutip_qip/circuit/draw/text_renderer.py b/src/qutip_qip/circuit/draw/text_renderer.py index ce5b98838..4b755db03 100644 --- a/src/qutip_qip/circuit/draw/text_renderer.py +++ b/src/qutip_qip/circuit/draw/text_renderer.py @@ -151,7 +151,7 @@ def _draw_multiq_gate( sorted_targets = sorted(targets) # Adjust top_frame or bottom if there is a control wire - if gate.is_controlled_gate(): + if gate.is_controlled(): sorted_controls = sorted(controls) top_frame = ( (top_frame[:mid_index] + "┴" + top_frame[mid_index + 1 :]) @@ -492,7 +492,7 @@ def layout(self) -> None: parts, ) - if gate.is_controlled_gate(): + if gate.is_controlled(): sorted_controls = sorted(controls) # check if there is control wire above the gate top diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index 335e3dcbe..eb5600a3f 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -93,13 +93,13 @@ def __post_init__(self) -> None: @property def controls(self) -> tuple[int]: - if self.operation.is_controlled_gate(): + if self.operation.is_controlled(): return self.qubits[: self.operation.num_ctrl_qubits] return () @property def targets(self) -> tuple[int]: - if self.operation.is_controlled_gate(): + if self.operation.is_controlled(): return self.qubits[self.operation.num_ctrl_qubits :] return self.qubits diff --git a/src/qutip_qip/compiler/instruction.py b/src/qutip_qip/compiler/instruction.py index a04871846..84ed8726b 100644 --- a/src/qutip_qip/compiler/instruction.py +++ b/src/qutip_qip/compiler/instruction.py @@ -85,6 +85,6 @@ def controls(self): :type: list """ - if self.gate.is_controlled_gate(): + if self.gate.is_controlled(): return self._controls return None diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index a8fb0e078..3e6c28c18 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -186,7 +186,7 @@ def inverse(self) -> Gate: ) @staticmethod - def is_controlled_gate() -> bool: + def is_controlled() -> bool: return True @classmethod diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index e1a381ff3..1e1464dfc 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -228,7 +228,7 @@ def inverse(cls) -> Gate: raise NotImplementedError @staticmethod - def is_controlled_gate() -> bool: + def is_controlled() -> bool: """ Check if the gate is a controlled gate. diff --git a/tests/test_circuit.py b/tests/test_circuit.py index e0a6f1ca1..5d4d71c6b 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -228,7 +228,7 @@ def test_add_circuit(self): if qc1.instructions[i].is_gate_instruction() and ( qc.instructions[i].is_gate_instruction() ): - if qc.instructions[i].operation.is_controlled_gate(): + if qc.instructions[i].operation.is_controlled(): assert ( qc1.instructions[i].controls == qc.instructions[i].controls @@ -259,7 +259,7 @@ def test_add_circuit(self): qc2.instructions[i].targets[0] == qc.instructions[i].targets[0] + 2 ) - if qc.instructions[i].operation.is_controlled_gate(): + if qc.instructions[i].operation.is_controlled(): assert ( qc2.instructions[i].controls[0] == qc.instructions[i].controls[0] + 2 diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index d30df6bc4..96bc3ce48 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -74,13 +74,13 @@ def _compare_gate_instructions( else: # TODO correct for float error in arg_value res_controls = None - if res_gate.operation.is_controlled_gate(): + if res_gate.operation.is_controlled(): res_controls = get_qutip_index( list(res_gate.controls), result_circuit.num_qubits ) req_controls = None - if req_gate.operation.is_controlled_gate(): + if req_gate.operation.is_controlled(): req_controls = list(req_gate.controls) check_condition = ( diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 9c66cffdf..bd61a6ee2 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -66,7 +66,7 @@ def test_qpe_circuit_structure(self): for i in range(num_counting): circ_instruction = circuit.instructions[num_counting + i] - assert_(circ_instruction.operation.is_controlled_gate) + assert_(circ_instruction.operation.is_controlled) assert_equal(circ_instruction.controls, [i]) assert_equal(circ_instruction.targets, [num_counting]) From b31f5f4430215981a64eec705ee76321b4092b67 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 24 Feb 2026 00:25:59 +0530 Subject: [PATCH 027/117] Rename is_parametric_gate method to is_parametric --- src/qutip_qip/circuit/_decompose.py | 9 ++- src/qutip_qip/circuit/circuit.py | 9 +-- src/qutip_qip/circuit/draw/mat_renderer.py | 2 +- src/qutip_qip/circuit/draw/texrenderer.py | 2 +- src/qutip_qip/circuit/draw/text_renderer.py | 2 +- src/qutip_qip/circuit/instruction.py | 2 +- src/qutip_qip/compiler/gatecompiler.py | 2 +- src/qutip_qip/operations/__init__.py | 1 - src/qutip_qip/operations/controlled.py | 16 +++--- src/qutip_qip/operations/gateclass.py | 6 +- src/qutip_qip/operations/parametric.py | 2 +- .../operations/std/single_qubit_gate.py | 1 + src/qutip_qip/qiskit/utils/converter.py | 4 +- tests/test_gates.py | 55 ++++++++++++++----- tests/test_qiskit.py | 4 +- 15 files changed, 75 insertions(+), 42 deletions(-) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 4fb8eb5f0..330c0cdbb 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -6,7 +6,14 @@ import numpy as np from qutip_qip.operations.std import ( - RX, RY, RZ, CX, ISWAP, SQRTSWAP, SQRTISWAP, CZ + RX, + RY, + RZ, + CX, + ISWAP, + SQRTSWAP, + SQRTISWAP, + CZ, ) __all__ = ["_resolve_to_universal", "_resolve_2q_basis"] diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 6c6a49eef..2998089f7 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -16,7 +16,7 @@ ) from qutip_qip.circuit.utils import _check_iterable, _check_limit_ from qutip_qip.operations import Gate, Measurement, expand_operator -from qutip_qip.operations.std import RX, RY,RZ, GLOBALPHASE, GATE_CLASS_MAP +from qutip_qip.operations.std import RX, RY, RZ, GLOBALPHASE, GATE_CLASS_MAP from qutip_qip.typing import IntList try: @@ -355,17 +355,14 @@ def add_gate( "or Gate class or its object instantiation" ) - if ( - gate_class.is_controlled() - and gate_class.is_parametric_gate() - ): + if gate_class.is_controlled() and gate_class.is_parametric(): gate = gate_class( control_value=control_value, arg_value=arg_value, arg_label=arg_label, ) - elif gate_class.is_parametric_gate(): + elif gate_class.is_parametric(): gate = gate_class(arg_value=arg_value, arg_label=arg_label) elif gate_class.is_controlled(): diff --git a/src/qutip_qip/circuit/draw/mat_renderer.py b/src/qutip_qip/circuit/draw/mat_renderer.py index 8b62159b1..c707e86ea 100644 --- a/src/qutip_qip/circuit/draw/mat_renderer.py +++ b/src/qutip_qip/circuit/draw/mat_renderer.py @@ -784,7 +784,7 @@ def canvas_plot(self) -> None: style = style if style is not None else {} self.text = gate.name - if gate.is_parametric_gate(): + if gate.is_parametric(): self.text = ( gate.arg_label if gate.arg_label is not None diff --git a/src/qutip_qip/circuit/draw/texrenderer.py b/src/qutip_qip/circuit/draw/texrenderer.py index 5060de479..ae3d6a42c 100644 --- a/src/qutip_qip/circuit/draw/texrenderer.py +++ b/src/qutip_qip/circuit/draw/texrenderer.py @@ -42,7 +42,7 @@ def __init__(self, qc: QubitCircuit): def _gate_label(self, gate) -> str: gate_label = gate.latex_str - if gate.is_parametric_gate() and gate.arg_label is not None: + if gate.is_parametric() and gate.arg_label is not None: return rf"{gate_label}({gate.arg_label})" return rf"{gate_label}" diff --git a/src/qutip_qip/circuit/draw/text_renderer.py b/src/qutip_qip/circuit/draw/text_renderer.py index 4b755db03..0578b0623 100644 --- a/src/qutip_qip/circuit/draw/text_renderer.py +++ b/src/qutip_qip/circuit/draw/text_renderer.py @@ -440,7 +440,7 @@ def layout(self) -> None: targets = list(circ_instruction.targets) controls = list(circ_instruction.controls) - if gate.is_parametric_gate() and gate.arg_label is not None: + if gate.is_parametric() and gate.arg_label is not None: gate_text = gate.arg_label if gate.name == "SWAP": diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index eb5600a3f..c27991bb0 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -109,7 +109,7 @@ def is_gate_instruction(self) -> bool: def to_qasm(self, qasm_out) -> None: gate = self.operation args = None - if gate.is_parametric_gate(): + if gate.is_parametric(): args = gate.arg_value qasm_gate = qasm_out.qasm_name(gate.name) diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 37f3e2afd..96f80d2d4 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -117,7 +117,7 @@ def idle_compiler(self, circuit_instruction, args): """ idle_time = None gate = circuit_instruction.operation - if gate.is_parametric_gate(): + if gate.is_parametric(): idle_time = gate.arg_value return [PulseInstruction(circuit_instruction, idle_time, [])] diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 88cdaa3cc..2178ad8a3 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -139,7 +139,6 @@ "qubit_clifford_group", "expand_operator", "gate_sequence_product", - # The below ones need to be removed (should only be maintained in ./std), no duplication "GATE_CLASS_MAP", "GLOBALPHASE", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 3e6c28c18..fb45f6119 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -77,7 +77,7 @@ def __init_subclass__(cls, **kwargs): ) # Automatically copy the validator from the target - if cls.target_gate.is_parametric_gate(): + if cls.target_gate.is_parametric(): cls.validate_params = staticmethod(cls.target_gate.validate_params) # Copy the num_params if not defined @@ -105,7 +105,7 @@ def __init__( else: self._control_value = (2**self.num_ctrl_qubits) - 1 - if self.is_parametric_gate(): + if self.is_parametric(): ParametricGate.__init__( self, arg_value=arg_value, arg_label=arg_label ) @@ -158,7 +158,7 @@ def get_qobj(self) -> Qobj: The unitary matrix representing the controlled operation. """ target_gate = self.target_gate - if self.is_parametric_gate(): + if self.is_parametric(): target_gate = target_gate(self.arg_value) return controlled_gate_unitary( @@ -168,7 +168,7 @@ def get_qobj(self) -> Qobj: ) def inverse(self) -> Gate: - if not self.is_parametric_gate(): + if not self.is_parametric(): inverse_gate = controlled_gate( self.target_gate.inverse(), self.num_ctrl_qubits ) @@ -180,9 +180,9 @@ def inverse(self) -> Gate: inverse_gate = controlled_gate( type(inverse_target_gate), self.num_ctrl_qubits ) - + return inverse_gate( - control_value = self.control_value, arg_value=arg_value + control_value=self.control_value, arg_value=arg_value ) @staticmethod @@ -190,8 +190,8 @@ def is_controlled() -> bool: return True @classmethod - def is_parametric_gate(cls) -> bool: - return cls.target_gate.is_parametric_gate() + def is_parametric(cls) -> bool: + return cls.target_gate.is_parametric() def __str__(self) -> str: return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 1e1464dfc..1e970c4b7 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -94,7 +94,7 @@ class X(Gate): This is required since num_qubits etc. are class attributes (shared by all object instances). """ # Check if the attribute is in our protected set - # Using cls.__dict__ ignores parent classes, allowing __init_subclass__ + # Using cls.__dict__ ignores parent classes, allowing __init_subclass__ # to set it once for the child class. if name in cls._read_only_set and name in cls.__dict__: raise AttributeError(f"{name} is read-only!") @@ -239,7 +239,7 @@ def is_controlled() -> bool: return False @staticmethod - def is_parametric_gate() -> bool: + def is_parametric() -> bool: """ Check if the gate accepts variable parameters (e.g., rotation angles). @@ -278,7 +278,7 @@ class _CustomGate(Gate): _namespace = namespace name = gate_name num_qubits = int(n) - self_inverse = (U == U.dag()) + self_inverse = U == U.dag() @staticmethod def get_qobj(): diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index eaa4d14c7..574d6f124 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -105,7 +105,7 @@ def get_qobj(self) -> Qobj: pass @staticmethod - def is_parametric_gate(): + def is_parametric(): return True def __str__(self): diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index df7726c1e..974a48e2d 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -143,6 +143,7 @@ class SNOT(H): """ Hadamard gate (Deprecated, use H instead). """ + __slots__ = () def __init__(self): diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index faf8ae773..444d99409 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -118,7 +118,7 @@ def convert_qiskit_circuit_to_qutip( # add the corresponding gate in qutip_qip if qiskit_instruction.name in _map_gates.keys(): gate = _map_gates[qiskit_instruction.name] - if gate.is_parametric_gate(): + if gate.is_parametric(): gate = gate(arg_value) qutip_circuit.add_gate( @@ -128,7 +128,7 @@ def convert_qiskit_circuit_to_qutip( elif qiskit_instruction.name in _map_controlled_gates.keys(): gate = _map_controlled_gates[qiskit_instruction.name] - if gate.is_parametric_gate(): + if gate.is_parametric(): gate = gate(arg_value) # FIXME This doesn't work for multicontrolled gates diff --git a/tests/test_gates.py b/tests/test_gates.py index efc0fce15..6222ce6fa 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -471,33 +471,62 @@ def test_gates_class(): assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 + GATES = [ - std.X, std.Y, std.Z, std.H, std.S, std.Sdag, std.T, std.Tdag, - std.SWAP, std.ISWAP, std.ISWAPdag, std.SQRTSWAP, std.SQRTISWAPdag, - std.SQRTISWAP, std.SQRTISWAPdag, std.BERKELEY, std.BERKELEYdag + std.X, + std.Y, + std.Z, + std.H, + std.S, + std.Sdag, + std.T, + std.Tdag, + std.SWAP, + std.ISWAP, + std.ISWAPdag, + std.SQRTSWAP, + std.SQRTISWAPdag, + std.SQRTISWAP, + std.SQRTISWAPdag, + std.BERKELEY, + std.BERKELEYdag, ] PARAMETRIC_GATE = [ - std.RX(0.5), std.RY(0.5), std.RZ(0.5), std.PHASE(0.5), std.R((0.5, 0.9)), - std.QASMU((0.1, 0.2, 0.3)), std.SWAPALPHA(0.3), std.MS((0.47, 0.8)), + std.RX(0.5), + std.RY(0.5), + std.RZ(0.5), + std.PHASE(0.5), + std.R((0.5, 0.9)), + std.QASMU((0.1, 0.2, 0.3)), + std.SWAPALPHA(0.3), + std.MS((0.47, 0.8)), std.RZX(0.6), ] CONTROLLED_GATE = [ - std.CX(), std.CY(), std.CZ(), std.CH(), std.CS(), std.CT(), - std.CRX(arg_value=0.7), std.CRY(arg_value=0.88), std.CRZ(arg_value=0.78), - std.CPHASE(arg_value=0.9), std.CQASMU(arg_value=[0.9, 0.22, 0.15]), - std.TOFFOLI(), std.FREDKIN() + std.CX(), + std.CY(), + std.CZ(), + std.CH(), + std.CS(), + std.CT(), + std.CRX(arg_value=0.7), + std.CRY(arg_value=0.88), + std.CRZ(arg_value=0.78), + std.CPHASE(arg_value=0.9), + std.CQASMU(arg_value=[0.9, 0.22, 0.15]), + std.TOFFOLI(), + std.FREDKIN(), ] -@pytest.mark.parametrize( - "gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE -) + +@pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) def test_gate_inverse(gate: Gate): n = 2**gate.num_qubits inverse_gate = gate.inverse() np.testing.assert_allclose( (gate.get_qobj() * inverse_gate.get_qobj()).full(), np.eye(n), - atol=1e-12 + atol=1e-12, ) diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 96bc3ce48..62fe32a7e 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -38,11 +38,11 @@ class TestConverter: def _compare_args(self, req_gate, res_gate): """Compare parameters of two gates""" res_arg = [] - if res_gate.operation.is_parametric_gate(): + if res_gate.operation.is_parametric(): res_arg = res_gate.operation.arg_value req_arg = [] - if req_gate.operation.is_parametric_gate(): + if req_gate.operation.is_parametric(): req_arg = req_gate.operation.arg_value if len(req_arg) != len(res_arg): From 81c12d528f0f6c9c8e24a85722832e97dc626bd1 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 24 Feb 2026 01:01:33 +0530 Subject: [PATCH 028/117] Fix dagger symbol in latex_str for gates --- src/qutip_qip/operations/std/single_qubit_gate.py | 10 +++++----- src/qutip_qip/operations/std/two_qubit_gate.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 974a48e2d..c69f766bc 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -174,7 +174,7 @@ class SQRTX(_SingleQubitGate): self_inverse = False is_clifford = True - latex_str = r"\sqrt{\rm NOT}" + latex_str = r"\sqrt{\rm X}" @staticmethod def get_qobj() -> Qobj: @@ -187,7 +187,7 @@ def inverse() -> Gate: class SQRTXdag(_SingleQubitGate): r""" - :math:`\sqrt{X}^{\dag}` gate. + :math:`\sqrt{X}^\dagger` gate. Examples -------- @@ -203,7 +203,7 @@ class SQRTXdag(_SingleQubitGate): self_inverse = False is_clifford = True - latex_str = r"\sqrt{\rm Xdag}" + latex_str = r"\sqrt{\rm X}^\dagger" @staticmethod def get_qobj() -> Qobj: @@ -274,7 +274,7 @@ class Sdag(_SingleQubitGate): self_inverse = False is_clifford = True - latex_str = r"{\rm Sdag}" + latex_str = r"{\rm S^\dagger}" @staticmethod def get_qobj() -> Qobj: @@ -315,7 +315,7 @@ def inverse() -> Gate: class Tdag(_SingleQubitGate): r""" - Tdag gate or :math:`\sqrt[4]{Z}^{\dag}` gate. + Tdag gate or :math:`\sqrt[4]{Z}^\dagger` gate. Examples -------- diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 6aa00be1b..47fd8db3c 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -168,7 +168,7 @@ def inverse() -> Gate: class SQRTSWAPdag(_TwoQubitGate): r""" - :math:`\sqrt{\mathrm{SWAP}}^{\dag}` gate. + :math:`\sqrt{\mathrm{SWAP}}^\dagger` gate. Examples -------- @@ -185,7 +185,7 @@ class SQRTSWAPdag(_TwoQubitGate): __slots__ = () self_inverse = False - latex_str = r"\sqrt{\rm SWAP}^{\dag}" + latex_str = r"\sqrt{\rm SWAP}^\dagger" @staticmethod def get_qobj(): @@ -249,7 +249,7 @@ def inverse() -> Gate: class SQRTISWAPdag(_TwoQubitGate): r""" - :math:`\sqrt{\mathrm{iSWAP}}^{\dag}` gate. + :math:`\sqrt{\mathrm{iSWAP}}^\dagger` gate. Examples -------- @@ -267,7 +267,7 @@ class SQRTISWAPdag(_TwoQubitGate): self_inverse = False is_clifford = True - latex_str = r"\sqrt{{i}\rm SWAP}^{\dag}" + latex_str = r"\sqrt{{i}\rm SWAP}^\dagger" @staticmethod def get_qobj(): @@ -363,7 +363,7 @@ class BERKELEYdag(_TwoQubitGate): __slots__ = () self_inverse = False - latex_str = r"{\rm BERKELEY^{\dag}}" + latex_str = r"{\rm BERKELEY^\dagger}" @staticmethod def get_qobj(): From d28cd8d566857e3d94ea6d9b63f97305b7dda9d0 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 05:48:22 +0530 Subject: [PATCH 029/117] Added further checks in add_gate Specifically that targets, controls are in accordance with the gate and also readded a test in test_gate.py for random controlled 3 qubit gate and skipped test for the clifford group as deprecated in gates.py --- src/qutip_qip/circuit/circuit.py | 15 +++++++++---- tests/test_circuit.py | 5 ++--- tests/test_gates.py | 37 ++++++++------------------------ 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 2998089f7..3193545a4 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -248,12 +248,12 @@ def add_measurement( def add_gate( self, gate: Gate | str, - targets: Iterable[int] = [], - controls: Iterable[int] = [], + targets: int | Iterable[int] = [], + controls: int | Iterable[int] = [], arg_value: any = None, arg_label: str | None = None, control_value: int | None = None, - classical_controls: Iterable[int] = [], + classical_controls: int | Iterable[int] = [], classical_control_value: int | None = None, style: dict = None, index: None = None, @@ -337,7 +337,8 @@ def add_gate( if len(classical_controls) > 0 and classical_control_value is None: classical_control_value = 2 ** (len(classical_controls)) - 1 - # This can be remove if the gate input is only restricted to Gate or its object instead of strings + # This conditional block can be remove if the gate input is only + # restricted to Gate subclasses or object instead of strings in the future. if not isinstance(gate, Gate): if type(gate) is str and gate in GATE_CLASS_MAP: warnings.warn( @@ -370,6 +371,12 @@ def add_gate( else: gate = gate_class + if gate.is_controlled() and len(controls) != gate.num_ctrl_qubits: + raise ValueError(f"{gate.name} takes {gate.num_ctrl_qubits} qubits, but {len(controls)} were provided.") + + if len(controls) + len(targets) != gate.num_qubits: + raise ValueError(f"{gate.name} takes {gate.num_qubits} qubits, but {len(controls) + len(targets)} were provided.") + qubits = [] if controls is not None: qubits.extend(controls) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 5d4d71c6b..0b6dd5b2c 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -325,8 +325,7 @@ def test_add_measurement(self): assert qc.instructions[2].operation.name == "TOFFOLI" assert qc.instructions[4].cbits == (0, 1) - @pytest.mark.skip(reason="Changing the interface completely") - @pytest.mark.parametrize("gate", ["X", "Y", "Z", "S", "T"]) + @pytest.mark.parametrize("gate", [std.X, std.Y, std.Z, std.S, std.T]) def test_exceptions(self, gate): """ Text exceptions are thrown correctly for inadequate inputs @@ -740,7 +739,7 @@ def test_latex_code_non_reversed(self): shutil.which("pdflatex") is None, reason="requires pdflatex" ) def test_export_image(self, in_temporary_directory): - from qutip_qip.circuit.texrenderer import CONVERTERS + from qutip_qip.circuit.draw import CONVERTERS qc = QubitCircuit(2, reverse_states=False) qc.add_gate(std.CZ, controls=[0], targets=[1]) diff --git a/tests/test_gates.py b/tests/test_gates.py index 6222ce6fa..4fe0b0c6f 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -5,7 +5,9 @@ import qutip from qutip.core.gates import hadamard_transform, qubit_clifford_group from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import Gate, expand_operator +from qutip_qip.operations import ( + Gate, expand_operator, controlled_gate, unitary_gate +) import qutip_qip.operations.std as std @@ -18,20 +20,6 @@ def _infidelity(a, b): return 1 - abs(a.overlap(b)) -def _make_random_three_qubit_gate(): - """Create a random three-qubit gate.""" - operation = qutip.rand_unitary([2] * 3) - - def gate(N=None, controls=None, target=None): - if N is None: - return operation - return expand_operator( - operation, dims=[2] * N, targets=controls + [target] - ) - - return gate - - def _tensor_with_entanglement(all_qubits, entangled, entangled_locations): """ Create a full tensor product when a subspace component is already in an @@ -148,6 +136,7 @@ def test_zero_rotations_are_identity(self, gate, n_angles): ) +@pytest.mark.skip("qubit_clifford_group was deprecated in gates.py") class TestCliffordGroup: """ Test a sufficient set of conditions to prove that we have a full Clifford @@ -168,7 +157,7 @@ def test_all_elements_different(self): fid = qutip.average_gate_fidelity(gate, other) assert not np.allclose(fid, 1.0, atol=1e-3) - @pytest.mark.parametrize("gate", qubit_clifford_group()) + @pytest.mark.parametrize("gate", clifford) def test_gate_normalises_pauli_group(self, gate): """ Test the fundamental definition of the Clifford group, i.e. that it @@ -259,22 +248,14 @@ def test_two_qubit(self, gate, n_controls): expected = _tensor_with_entanglement(qubits, reference, [q1, q2]) assert _infidelity(test, expected) < 1e-12 - # class RandomThreeQubitGate(ControlledGate): - # num_qubits = 3 - # num_ctrl_qubits = 2 - # target_gate = None - - # def get_qobj(self): - # if self._U is None: - # self._U = _make_random_three_qubit_gate() - # return self._U - + random_gate = unitary_gate("random", qutip.rand_unitary([2] * 1)) + RandomThreeQubitGate = controlled_gate(random_gate, 2) @pytest.mark.parametrize( ["gate", "n_controls"], [ pytest.param(std.FREDKIN(), 1, id="Fredkin"), pytest.param(std.TOFFOLI(), 2, id="Toffoli"), - # pytest.param(RandomThreeQubitGate(), 2, id="random"), + pytest.param(RandomThreeQubitGate(), 2, id="random"), ], ) def test_three_qubit(self, gate: Gate, n_controls): @@ -464,7 +445,7 @@ def test_gates_class(): circuit2.add_gate(std.SWAPALPHA(np.pi / 4), targets=[1, 2]) circuit2.add_gate(std.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0]) circuit2.add_gate(std.TOFFOLI, controls=[2, 0], targets=[1]) - circuit2.add_gate(std.FREDKIN, controls=[0, 1], targets=[2]) + circuit2.add_gate(std.FREDKIN, controls=[0], targets=[1, 2]) circuit2.add_gate(std.BERKELEY, targets=[1, 0]) circuit2.add_gate(std.RZX(arg_value=1.0), targets=[1, 0]) result2 = circuit2.run(init_state) From 853a2c4f614f90fef651d0b9496174daf2ae6090 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 05:52:00 +0530 Subject: [PATCH 030/117] Renamed gates.py to old_gates.py --- src/qutip_qip/operations/__init__.py | 2 +- src/qutip_qip/operations/{gates.py => old_gates.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/qutip_qip/operations/{gates.py => old_gates.py} (100%) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 2178ad8a3..113e3d3a7 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -11,7 +11,7 @@ from .parametric import ParametricGate, AngleParametricGate from .controlled import ControlledGate, controlled_gate from .measurement import Measurement -from .gates import ( +from .old_gates import ( rx, ry, rz, diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/old_gates.py similarity index 100% rename from src/qutip_qip/operations/gates.py rename to src/qutip_qip/operations/old_gates.py From 51978c5df5b95ab744ed6a94eb389513f1165a02 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 06:12:23 +0530 Subject: [PATCH 031/117] Removed deprecated variables from expand_operator Specifically N, cyclic_permutation as they were deprecated 4 years ago and need to be removed now. --- src/qutip_qip/operations/utils.py | 67 +++++++------------------------ tests/test_gates.py | 4 +- 2 files changed, 16 insertions(+), 55 deletions(-) diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 3f138a4a1..c9cfedb78 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -1,11 +1,9 @@ -import warnings import numbers -from typing import Sequence from collections.abc import Iterable import numpy as np -from scipy.linalg import block_diag import qutip +from scipy.linalg import block_diag from qutip import Qobj, identity, tensor @@ -62,14 +60,14 @@ def _targets_to_list(targets, oper=None, N=None): targets = [targets] if not all([isinstance(t, numbers.Integral) for t in targets]): raise TypeError("targets should be an integer or a list of integer") + # if targets has correct length if oper is not None: req_num = len(oper.dims[0]) if len(targets) != req_num: raise ValueError( f"The given operator needs {req_num} " - "target qutbis, " - "but {len(targets)} given." + f"target qubits, but {len(targets)} given." ) # If targets is smaller than N @@ -81,10 +79,8 @@ def _targets_to_list(targets, oper=None, N=None): def expand_operator( oper: Qobj, - N: None = None, - targets: int | list[int] | None = None, - dims: list[int] | None = None, - cyclic_permutation: bool = False, + dims: Iterable[int], + targets: int | Iterable[int] | None = None, dtype: str | None = None, ): """ @@ -102,14 +98,6 @@ def expand_operator( targets : int or list of int The indices of subspace that are acted on. Permutation can also be realized by changing the orders of the indices. - N : int - Deprecated. Number of qubits. Please use `dims`. - cyclic_permutation : boolean, optional - Deprecated. - Expand for all cyclic permutation of the targets. - E.g. if ``N=3`` and `oper` is a 2-qubit operator, - the result will be a list of three operators, - each acting on qubits 0 and 1, 1 and 2, 2 and 0. dtype : str, optional Data type of the output `Qobj`. Only for qutip version larger than 5. @@ -121,9 +109,9 @@ def expand_operator( Examples -------- - >>> from qutip_qip.operations import expand_operator - >>> from qutip_qip.operations.std import X, CNOT >>> import qutip + >>> from qutip_qip.operations import expand_operator + >>> from qutip_qip.operations.std import X, CX >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True Qobj data = @@ -133,7 +121,7 @@ def expand_operator( [1. 0. 0. 0. 0. 0.] [0. 1. 0. 0. 0. 0.] [0. 0. 1. 0. 0. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE + >>> expand_operator(CX().get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -144,7 +132,7 @@ def expand_operator( [0. 0. 0. 0. 0. 1. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 1.] [0. 0. 0. 0. 0. 0. 1. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE + >>> expand_operator(CX().get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -159,42 +147,13 @@ def expand_operator( dtype = dtype or qutip.settings.core["default_dtype"] or qutip.data.CSR oper = oper.to(dtype) - if N is not None: - warnings.warn( - "The function expand_operator has been generalized to " - "arbitrary subsystems instead of only qubit systems." - "Please use the new signature e.g.\n" - "expand_operator(oper, dims=[2, 3, 2, 2], targets=2)", - DeprecationWarning, - stacklevel=2, - ) - - if dims is not None and N is None: - if not isinstance(dims, Iterable): - f"dims needs to be an interable {not type(dims)}." - N = len(dims) # backward compatibility + if not isinstance(dims, Iterable): + raise ValueError(f"dims needs to be an interable {not type(dims)}.") - if dims is None: - dims = [2] * N + N = len(dims) targets = _targets_to_list(targets, oper=oper, N=N) _check_oper_dims(oper, dims=dims, targets=targets) - # Call expand_operator for all cyclic permutation of the targets. - if cyclic_permutation: - warnings.warn( - "cyclic_permutation is deprecated, " - "please use loop through different targets manually.", - DeprecationWarning, - stacklevel=2, - ) - oper_list = [] - for i in range(N): - new_targets = np.mod(np.array(targets) + i, N) - oper_list.append( - expand_operator(oper, N=N, targets=new_targets, dims=dims) - ) - return oper_list - # Generate the correct order for permutation, # eg. if N = 5, targets = [3,0], the order is [1,2,3,0,4]. # If the operator is cnot, @@ -202,12 +161,14 @@ def expand_operator( new_order = [0] * N for i, t in enumerate(targets): new_order[t] = i + # allocate the rest qutbits (not targets) to the empty # position in new_order rest_pos = [q for q in list(range(N)) if q not in targets] rest_qubits = list(range(len(targets), N)) for i, ind in enumerate(rest_pos): new_order[ind] = rest_qubits[i] + id_list = [identity(dims[i]) for i in rest_pos] out = tensor([oper] + id_list).permute(new_order) return out.to(dtype) diff --git a/tests/test_gates.py b/tests/test_gates.py index 4fe0b0c6f..f69f8d6e1 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -213,7 +213,7 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): for target in range(self.n_qubits): start = qutip.tensor(random[:target] + [base] + random[target:]) test = ( - expand_operator(gate.get_qobj(), self.n_qubits, target) * start + expand_operator(gate.get_qobj(), targets=target, dims=[2]*self.n_qubits) * start ) expected = qutip.tensor( random[:target] + [applied] + random[target:] @@ -267,7 +267,7 @@ def test_three_qubit(self, gate: Gate, n_controls): qubits = others.copy() qubits[q1], qubits[q2], qubits[q3] = targets test = expand_operator( - gate.get_qobj(), self.n_qubits, [q1, q2, q3] + gate.get_qobj(), targets=[q1, q2, q3], dims=[2]*self.n_qubits ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement( qubits, reference, [q1, q2, q3] From 470f58caa8f68b45b6d4fde603fedeb039aa9fee Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 06:22:58 +0530 Subject: [PATCH 032/117] Removed deprecation for Gate, Measurement from circuit/ module --- src/qutip_qip/circuit/__init__.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/qutip_qip/circuit/__init__.py b/src/qutip_qip/circuit/__init__.py index 01799f66d..de8d57221 100644 --- a/src/qutip_qip/circuit/__init__.py +++ b/src/qutip_qip/circuit/__init__.py @@ -1,7 +1,5 @@ """Circuit representation and simulation at the gate level.""" -import warnings - from .instruction import ( CircuitInstruction, GateInstruction, @@ -9,32 +7,6 @@ ) from .simulator import CircuitResult, CircuitSimulator from .circuit import QubitCircuit -from qutip_qip.operations import Gate, Measurement - - -def _add_deprecation(fun, msg): - def newfun(*args, **kwargs): - warnings.warn( - msg, - DeprecationWarning, - stacklevel=2, - ) - return fun(*args, **kwargs) - - return newfun - - -Gate = _add_deprecation( - Gate, - "The class Gate has been moved to qutip_qip.operations." - "Please use update the import statement.\n", -) -Measurement = _add_deprecation( - Measurement, - "The class Measurement has been moved to qutip_qip.operations." - "Please use update the import statement.\n", -) - __all__ = [ "CircuitSimulator", From c7821d081fdd248f16f358a182f95068d649d5bf Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 06:23:42 +0530 Subject: [PATCH 033/117] Remove precompute_unitary argument from CircuitSimulator This was deprectaed several years ago and also was not present in the documentation any more. --- src/qutip_qip/circuit/circuit.py | 13 ++++--------- .../circuit/simulator/matrix_mul_simulator.py | 12 +----------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 3193545a4..f871d9e4a 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -116,7 +116,6 @@ def gates(self) -> list[CircuitInstruction]: return self._instructions gates.setter - def gates(self) -> None: warnings.warn( "QubitCircuit.gates has been replaced with QubitCircuit.instructions", @@ -530,7 +529,6 @@ def run( state, cbits=None, measure_results=None, - precompute_unitary=False, ): """ Calculate the result of one instance of circuit run. @@ -558,14 +556,11 @@ def run( mode = "density_matrix_simulator" else: raise TypeError("State is not a ket or a density matrix.") - sim = CircuitSimulator( - self, - mode, - precompute_unitary, - ) + + sim = CircuitSimulator(self, mode) return sim.run(state, cbits, measure_results).get_final_states(0) - def run_statistics(self, state, cbits=None, precompute_unitary=False): + def run_statistics(self, state, cbits=None): """ Calculate all the possible outputs of a circuit (varied by measurement gates). @@ -589,7 +584,7 @@ def run_statistics(self, state, cbits=None, precompute_unitary=False): mode = "density_matrix_simulator" else: raise TypeError("State is not a ket or a density matrix.") - sim = CircuitSimulator(self, mode, precompute_unitary) + sim = CircuitSimulator(self, mode) return sim.run_statistics(state, cbits) def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): diff --git a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py index 39f17b9f3..5869e067a 100644 --- a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py +++ b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py @@ -6,7 +6,6 @@ from qutip import ket2dm, Qobj from qutip_qip.circuit.simulator import CircuitResult from qutip_qip.operations import expand_operator -import warnings def _decimal_to_binary(decimal, length): @@ -35,12 +34,7 @@ class CircuitSimulator: Operator based circuit simulator. """ - def __init__( - self, - qc, - mode: str = "state_vector_simulator", - precompute_unitary: bool = False, - ): + def __init__(self, qc, mode: str = "state_vector_simulator") -> None: """ Simulate state evolution for Quantum Circuits. @@ -66,10 +60,6 @@ def __init__( self._qc = qc self.dims = qc.dims self.mode = mode - if precompute_unitary: - warnings.warn( - "Precomputing the full unitary is no longer supported. Switching to normal simulation mode." - ) @property def qc(self): From 6d82f78d2948c4626fe9a8a653cfcb60c4d546ea Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 06:26:56 +0530 Subject: [PATCH 034/117] Rename inverse() method for Gate to inverse_gate() --- src/qutip_qip/operations/controlled.py | 8 +++---- src/qutip_qip/operations/gateclass.py | 2 +- .../operations/std/single_qubit_gate.py | 24 +++++++++---------- .../operations/std/two_qubit_gate.py | 24 +++++++++---------- tests/test_gates.py | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index fb45f6119..480c50f64 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -39,7 +39,7 @@ class ControlledGate(Gate): num_ctrl_qubits: int target_gate: Gate - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs) -> None: """ Validates the subclass definition. """ @@ -167,15 +167,15 @@ def get_qobj(self) -> Qobj: control_value=self.control_value, ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: if not self.is_parametric(): inverse_gate = controlled_gate( - self.target_gate.inverse(), self.num_ctrl_qubits + self.target_gate.inverse_gate(), self.num_ctrl_qubits ) return inverse_gate(control_value=self.control_value) else: - inverse_target_gate = self.target_gate(self.arg_value).inverse() + inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() arg_value = inverse_target_gate.arg_value inverse_gate = controlled_gate( type(inverse_target_gate), self.num_ctrl_qubits diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 1e970c4b7..872c2aa5a 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -211,7 +211,7 @@ def get_qobj() -> Qobj: pass @classmethod - def inverse(cls) -> Gate: + def inverse_gate(cls) -> Gate: """ Return the inverse of the gate. diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index c69f766bc..fe216e4d8 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -181,7 +181,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTXdag @@ -210,7 +210,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTX @@ -252,7 +252,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, 1j]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return Sdag @@ -281,7 +281,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, -1j]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return S @@ -309,7 +309,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return Tdag @@ -337,7 +337,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return T @@ -370,7 +370,7 @@ def get_qobj(self) -> Qobj: dims=[[2], [2]], ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta = self.arg_value[0] return RX(-theta) @@ -403,7 +403,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta = self.arg_value[0] return RY(-theta) @@ -431,7 +431,7 @@ def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta = self.arg_value[0] return RZ(-theta) @@ -459,7 +459,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: phi = self.arg_value[0] return PHASE(-phi) @@ -505,7 +505,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: phi, theta = self.arg_value return R([phi, -theta]) @@ -547,6 +547,6 @@ def get_qobj(self) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta, phi, gamma = self.arg_value return QASMU([-theta, -gamma, -phi]) diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 47fd8db3c..5630a0b1c 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -88,7 +88,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return ISWAPdag @@ -122,7 +122,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return ISWAP @@ -162,7 +162,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTSWAPdag @@ -202,7 +202,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTSWAP @@ -243,7 +243,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTISWAPdag @@ -284,7 +284,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return SQRTISWAP @@ -331,7 +331,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return BERKELEYdag @@ -378,7 +378,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse_gate() -> Gate: return BERKELEY @@ -435,7 +435,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: alpha = self.arg_value[0] return SWAPALPHA(-alpha) @@ -493,7 +493,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta, phi = self.arg_value return MS([-theta, phi]) @@ -543,7 +543,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse_gate(self) -> Gate: theta = self.arg_value[0] return RZX(-theta) @@ -666,7 +666,7 @@ class CH(_ControlledTwoQubitGate): target_gate = H latex_str = r"{\rm CH}" - def inverse(self): + def inverse_gate(self): return CH(self.control_value) diff --git a/tests/test_gates.py b/tests/test_gates.py index f69f8d6e1..40e638a43 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -505,7 +505,7 @@ def test_gates_class(): @pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) def test_gate_inverse(gate: Gate): n = 2**gate.num_qubits - inverse_gate = gate.inverse() + inverse_gate = gate.inverse_gate() np.testing.assert_allclose( (gate.get_qobj() * inverse_gate.get_qobj()).full(), np.eye(n), From e070a78174d4e5b4f03019e869c59527c6b142f0 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 13:26:38 +0530 Subject: [PATCH 035/117] Add typehints to operations/utils.py Specifically to expand_operand, gate_product_sequence and their helper functions. --- src/qutip_qip/operations/utils.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index c9cfedb78..195a16c03 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -7,7 +7,11 @@ from qutip import Qobj, identity, tensor -def _check_oper_dims(oper: Qobj, dims=None, targets=None): +def _check_oper_dims( + oper: Qobj, + dims: Iterable[int] | None = None, + targets: Iterable[int] | None = None, +) -> None: """ Check if the given operator is valid. @@ -27,7 +31,8 @@ def _check_oper_dims(oper: Qobj, dims=None, targets=None): "The operator is not an " "Qobj with the same input and output dimensions." ) - # if operator dims matches the target dims + + # If operator dims matches the target dims if dims is not None and targets is not None: targ_dims = [dims[t] for t in targets] if oper.dims[0] != targ_dims: @@ -37,7 +42,11 @@ def _check_oper_dims(oper: Qobj, dims=None, targets=None): ) -def _targets_to_list(targets, oper=None, N=None): +def _targets_to_list( + targets: int | Iterable[int], + oper: Qobj | None = None, + N: int | None = None +) -> list[int]: """ transform targets to a list and check validity. @@ -82,7 +91,7 @@ def expand_operator( dims: Iterable[int], targets: int | Iterable[int] | None = None, dtype: str | None = None, -): +) -> Qobj: """ Expand an operator to one that acts on a system with desired dimensions. @@ -175,8 +184,11 @@ def expand_operator( def gate_sequence_product( - U_list, left_to_right=True, inds_list=None, expand=False -): + U_list: list[Qobj], + left_to_right: bool = True, + inds_list: list[list[int]] | None = None, + expand: bool = False +) -> Qobj | tuple[Qobj, list[int]]: """ Calculate the overall unitary matrix for a given list of unitary operations. From ff26e565189d9808d4c312bab012f0c5cf72ef02 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 25 Feb 2026 13:47:16 +0530 Subject: [PATCH 036/117] Minor changes --- src/qutip_qip/compiler/cavityqedcompiler.py | 5 +---- src/qutip_qip/device/processor.py | 2 +- src/qutip_qip/qasm.py | 18 ++---------------- tests/pytest.ini | 4 ++-- tests/test_gates.py | 5 ++--- 5 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 26673eaa4..78d35d100 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -116,10 +116,7 @@ def _rotation_compiler( args["num_samples"], maximum=self.params[param_label][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] - / 2.0 - / np.pi - * 0.5, + area=circuit_instruction.operation.arg_value[0] / (4.0 * np.pi), ) pulse_info = [(op_label + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 013add8e0..1d235326f 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -1120,7 +1120,7 @@ def run_state( init_state = states if analytical: if kwargs or self.noise: - raise warnings.warn( + raise warnings.warn( # FIXME this should raise an Error Type "Analytical matrices exponentiation" "does not process noise or" "any keyword arguments." diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 2b77bad31..1a7ec7340 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -543,8 +543,8 @@ def _add_qiskit_gates( "h": std.H, "t": std.T, "s": std.S, - # "sdg": sdg, - # "tdg": tdg, + "sdg": std.Sdag, + "tdg": std.Tdag, } if len(args) == 0: args = None @@ -573,20 +573,6 @@ def _add_qiskit_gates( classical_controls=classical_controls, classical_control_value=classical_control_value, ) - elif name == "sdg": - qc.add_gate( - std.RZ(-np.pi / 2), - targets=regs[0], - classical_controls=classical_controls, - classical_control_value=classical_control_value, - ) - elif name == "tdg": - qc.add_gate( - std.RZ(-np.pi / 4), - targets=regs[0], - classical_controls=classical_controls, - classical_control_value=classical_control_value, - ) elif name == "u1": qc.add_gate( std.RZ(args), diff --git a/tests/pytest.ini b/tests/pytest.ini index 7a448a001..b2071fe08 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -3,12 +3,12 @@ markers = slow: Mark a test as taking a long time to run, and so can be skipped with `pytest -m "not slow"`. repeat(n): Repeat the given test 'n' times. requires_cython: Mark that the given test requires Cython to be installed. Such tests will be skipped if Cython is not available. + filterwarnings = error ; ImportWarning: PyxImporter.find_spec() not found ignore:PyxImporter:ImportWarning - ; DeprecationWarning: Please use `upcast` from the `scipy.sparse` namespace - ignore::DeprecationWarning:qutip.fastsparse*: + ignore::DeprecationWarning:qutip_qip.operations*: ignore:matplotlib not found:UserWarning ignore:the imp module is deprecated in favour of importlib:DeprecationWarning ignore:Dedicated options class are no longer needed, options should be passed as dict to solvers.:FutureWarning diff --git a/tests/test_gates.py b/tests/test_gates.py index 40e638a43..75ab9a42f 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -3,10 +3,10 @@ import itertools import numpy as np import qutip -from qutip.core.gates import hadamard_transform, qubit_clifford_group +from qutip.core.gates import hadamard_transform from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ( - Gate, expand_operator, controlled_gate, unitary_gate + Gate, expand_operator, controlled_gate, unitary_gate, qubit_clifford_group ) import qutip_qip.operations.std as std @@ -136,7 +136,6 @@ def test_zero_rotations_are_identity(self, gate, n_angles): ) -@pytest.mark.skip("qubit_clifford_group was deprecated in gates.py") class TestCliffordGroup: """ Test a sufficient set of conditions to prove that we have a full Clifford From 3bb9f7f320679e33b1f5ac7ad581541cd9abe2c7 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 04:20:43 +0530 Subject: [PATCH 037/117] Corrected some latex_str for std gates --- src/qutip_qip/operations/std/single_qubit_gate.py | 6 +++--- src/qutip_qip/operations/std/two_qubit_gate.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index fe216e4d8..856e198b8 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -358,7 +358,7 @@ class RX(_SingleQubitParametricGate): __slots__ = () num_params = 1 - latex_str = r"R_x" + latex_str = r"RX" def get_qobj(self) -> Qobj: phi = self.arg_value[0] @@ -392,7 +392,7 @@ class RY(_SingleQubitParametricGate): __slots__ = () num_params = 1 - latex_str = r"R_y" + latex_str = r"RY" def get_qobj(self) -> Qobj: phi = self.arg_value[0] @@ -425,7 +425,7 @@ class RZ(_SingleQubitParametricGate): __slots__ = () num_params = 1 - latex_str = r"R_z" + latex_str = r"RZ" def get_qobj(self) -> Qobj: phi = self.arg_value[0] diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 5630a0b1c..fa8ae9a61 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -112,7 +112,7 @@ class ISWAPdag(_TwoQubitGate): self_inverse = False is_clifford = True - latex_str = r"{i}{\rm SWAPdag}" + latex_str = r"{i}{\rm SWAP^\dagger}" @staticmethod def get_qobj() -> Qobj: From c5903a284a1fbf0c0e5cc08eec74a4bf7f0c3502 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 16:22:42 +0530 Subject: [PATCH 038/117] Added ctrl_value to ControlledGate class --- src/qutip_qip/algorithms/qpe.py | 4 +- src/qutip_qip/circuit/circuit.py | 19 ++----- src/qutip_qip/operations/__init__.py | 4 +- src/qutip_qip/operations/controlled.py | 55 ++++++++++--------- src/qutip_qip/operations/std/other_gates.py | 2 + .../operations/std/two_qubit_gate.py | 18 +++--- tests/test_gates.py | 4 +- tests/test_qpe.py | 8 +-- tests/test_renderer.py | 8 +-- 9 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 7020144c5..7a06e7bc3 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,7 @@ import numpy as np from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import unitary_gate, controlled_gate, Gate +from qutip_qip.operations import unitary_gate, controlled, Gate from qutip_qip.operations.std import H @@ -63,7 +63,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): U_power = U if power == 1 else U**power # Add controlled-U^power gate - controlled_u = controlled_gate( + controlled_u = controlled( gate=unitary_gate( gate_name=f"U^{power}", namespace="qpe", diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index f871d9e4a..171b8459c 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -251,7 +251,7 @@ def add_gate( controls: int | Iterable[int] = [], arg_value: any = None, arg_label: str | None = None, - control_value: int | None = None, + control_value: None = None, classical_controls: int | Iterable[int] = [], classical_control_value: int | None = None, style: dict = None, @@ -277,7 +277,7 @@ def add_gate( Label for gate representation. classical_controls : int or list of int, optional Indices of classical bits to control the gate. - control_value : int, optional + control_value : optional Value of classical bits to control on, the classical controls are interpreted as an integer with the lowest bit being the first one. If not specified, then the value is interpreted to be @@ -299,8 +299,8 @@ def add_gate( if control_value is not None: warnings.warn( - "Define 'control_value', in your Gate object e.g. CX(control_value=0)" - ", 'control_value' argument will be removed from 'add_gate' method in the future version.", + "'control_value' is no longer a valid argument and has been deprecated and will be removed in the future version. " + "Use gate = controlled(gates.X, num_ctrl_qubits=1, control_value=0) instead", DeprecationWarning, stacklevel=2, ) @@ -355,18 +355,11 @@ def add_gate( "or Gate class or its object instantiation" ) - if gate_class.is_controlled() and gate_class.is_parametric(): - gate = gate_class( - control_value=control_value, - arg_value=arg_value, - arg_label=arg_label, - ) - - elif gate_class.is_parametric(): + if gate_class.is_parametric(): gate = gate_class(arg_value=arg_value, arg_label=arg_label) elif gate_class.is_controlled(): - gate = gate_class(control_value=control_value) + gate = gate_class() else: gate = gate_class diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 113e3d3a7..ef142f0cc 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -9,7 +9,7 @@ ) from .gateclass import Gate, unitary_gate from .parametric import ParametricGate, AngleParametricGate -from .controlled import ControlledGate, controlled_gate +from .controlled import ControlledGate, controlled from .measurement import Measurement from .old_gates import ( rx, @@ -100,7 +100,7 @@ "ParametricGate", "ControlledGate", "unitary_gate", - "controlled_gate", + "controlled", "AngleParametricGate", "Measurement", "rx", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 480c50f64..861437864 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -12,9 +12,15 @@ class ControlledGate(Gate): A controlled gate applies a target unitary operation only when the control qubits are in a specific state. - Parameters + Attributes ---------- - control_value : int, optional + num_ctrl_qubits : int + The number of qubits acting as controls. + + target_gate : Gate + The gate to be applied to the target qubits. + + ctrl_value : int The decimal value of the control state required to execute the unitary operator on the target qubits. @@ -23,20 +29,11 @@ class ControlledGate(Gate): set ``control_value=1``. * If the gate should execute when two control qubits are $|10\rangle$ (binary 10), set ``control_value=0b10``. - - Defaults to all-ones (e.g., $2^N - 1$) if not provided. - - Attributes - ---------- - num_ctrl_qubits : int - The number of qubits acting as controls. - - target_gate : Gate - The gate to be applied to the target qubits. """ __slots__ = ("arg_value", "arg_label", "_control_value") num_ctrl_qubits: int + ctrl_value: int target_gate: Gate def __init_subclass__(cls, **kwargs) -> None: @@ -164,25 +161,25 @@ def get_qobj(self) -> Qobj: return controlled_gate_unitary( U=target_gate.get_qobj(), num_controls=self.num_ctrl_qubits, - control_value=self.control_value, + control_value=self.ctrl_value, ) def inverse_gate(self) -> Gate: if not self.is_parametric(): - inverse_gate = controlled_gate( + inverse_gate = controlled( self.target_gate.inverse_gate(), self.num_ctrl_qubits ) - return inverse_gate(control_value=self.control_value) + return inverse_gate(control_value=self.ctrl_value) else: inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() arg_value = inverse_target_gate.arg_value - inverse_gate = controlled_gate( + inverse_gate = controlled( type(inverse_target_gate), self.num_ctrl_qubits ) return inverse_gate( - control_value=self.control_value, arg_value=arg_value + control_value=self.ctrl_value, arg_value=arg_value ) @staticmethod @@ -193,21 +190,23 @@ def is_controlled() -> bool: def is_parametric(cls) -> bool: return cls.target_gate.is_parametric() - def __str__(self) -> str: - return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" + @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: - if type(self) is not type(other): - return False + # def __eq__(self, other) -> bool: + # if type(self) is not type(other): + # return False - if self.control_value != other.control_value: - return False - return True + # if self.control_value != other.control_value: + # return False + # return True -def controlled_gate( +def controlled( gate: Gate, n_ctrl_qubits: int = 1, + control_value: int | None = None, gate_name: str | None = None, namespace: str = "custom", ) -> ControlledGate: @@ -218,12 +217,16 @@ def controlled_gate( if gate_name is None: gate_name = f"C{gate.name}" + if control_value is None: + control_value = 2**n_ctrl_qubits - 1 + class _CustomControlledGate(ControlledGate): __slots__ = () _namespace = namespace name = gate_name num_qubits = n_ctrl_qubits + gate.num_qubits num_ctrl_qubits = n_ctrl_qubits + ctrl_value = control_value target_gate = gate latex_str = r"{gate_name}" diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/std/other_gates.py index 27c2c46af..f6fb29864 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/std/other_gates.py @@ -63,6 +63,7 @@ class TOFFOLI(ControlledGate): num_qubits: int = 3 num_ctrl_qubits: int = 2 + ctrl_value = 0b11 target_gate = X latex_str = r"{\rm TOFFOLI}" @@ -91,5 +92,6 @@ class FREDKIN(ControlledGate): num_qubits: int = 3 num_ctrl_qubits: int = 1 + ctrl_value = 1 target_gate = SWAP latex_str = r"{\rm FREDKIN}" diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index fa8ae9a61..1b18f3ae3 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -19,13 +19,14 @@ class _ControlledTwoQubitGate(ControlledGate): """ This class allows correctly generating the gate instance when a redundant control_value is given, e.g. - ``CNOT(0, 1, control_value=1)``, + ``CX``, and raise an error if it is 0. """ __slots__ = () num_qubits: Final[int] = 2 num_ctrl_qubits: Final[int] = 1 + ctrl_value: Final[int] = 1 class SWAP(_TwoQubitGate): @@ -555,7 +556,7 @@ class CX(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations.std import CX - >>> CX(control_value=1).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> CX.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0.] @@ -574,14 +575,14 @@ class CX(_ControlledTwoQubitGate): class CNOT(CX): __slots__ = () - def __init__(self, control_value=None): + def __init__(self): warnings.warn( "CNOT is deprecated and will be removed in future versions. " "Use CX instead.", DeprecationWarning, stacklevel=2, ) - super().__init__(control_value) + super().__init__() class CY(_ControlledTwoQubitGate): @@ -602,8 +603,8 @@ class CY(_ControlledTwoQubitGate): __slots__ = () - target_gate = Y is_clifford = True + target_gate = Y latex_str = r"{\rm CY}" @@ -666,9 +667,6 @@ class CH(_ControlledTwoQubitGate): target_gate = H latex_str = r"{\rm CH}" - def inverse_gate(self): - return CH(self.control_value) - class CT(_ControlledTwoQubitGate): r""" @@ -777,9 +775,9 @@ class CRY(_ControlledTwoQubitGate): __slots__ = () - latex_str = r"{\rm CRY}" - target_gate = RY num_params: int = 1 + target_gate = RY + latex_str = r"{\rm CRY}" class CRZ(_ControlledTwoQubitGate): diff --git a/tests/test_gates.py b/tests/test_gates.py index 75ab9a42f..dc1aff632 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -6,7 +6,7 @@ from qutip.core.gates import hadamard_transform from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ( - Gate, expand_operator, controlled_gate, unitary_gate, qubit_clifford_group + Gate, expand_operator, controlled, unitary_gate, qubit_clifford_group ) import qutip_qip.operations.std as std @@ -248,7 +248,7 @@ def test_two_qubit(self, gate, n_controls): assert _infidelity(test, expected) < 1e-12 random_gate = unitary_gate("random", qutip.rand_unitary([2] * 1)) - RandomThreeQubitGate = controlled_gate(random_gate, 2) + RandomThreeQubitGate = controlled(random_gate, 2) @pytest.mark.parametrize( ["gate", "n_controls"], [ diff --git a/tests/test_qpe.py b/tests/test_qpe.py index bd61a6ee2..cdd561d90 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -4,7 +4,7 @@ from qutip import Qobj, sigmaz, tensor from qutip_qip.algorithms.qpe import qpe -from qutip_qip.operations import controlled_gate, unitary_gate +from qutip_qip.operations import controlled, unitary_gate class TestQPE(unittest.TestCase): @@ -28,11 +28,11 @@ def test_controlled_unitary(self): """ U = Qobj([[0, 1], [1, 0]]) - controlled_u = controlled_gate( + controlled_u = controlled( gate=unitary_gate(gate_name="CU", U=U), - )(control_value=1) + )() - assert_equal(controlled_u.control_value, 1) + assert_equal(controlled_u.ctrl_value, 1) assert_((controlled_u.target_gate.get_qobj() - U).norm() < 1e-12) def test_qpe_validation(self): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index fd80ccaa0..fd3ce806d 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -3,7 +3,7 @@ from unittest.mock import patch from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer -from qutip_qip.operations import controlled_gate +from qutip_qip.operations import controlled from qutip_qip.operations.std import ( IDLE, X, @@ -164,9 +164,9 @@ def qc3(): @pytest.fixture def qc4(): - i = controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="i") - ii = controlled_gate(IDLE, n_ctrl_qubits=2, gate_name="ii") - iii = controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="iii") + i = controlled(IDLE, n_ctrl_qubits=1, gate_name="i") + ii = controlled(IDLE, n_ctrl_qubits=2, gate_name="ii") + iii = controlled(IDLE, n_ctrl_qubits=1, gate_name="iii") qc = QubitCircuit(5, num_cbits=2) qc.add_gate( From cc54686978d631a971cb9d9d6c8462628a82b816 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 17:04:15 +0530 Subject: [PATCH 039/117] Remove mutable default values from function arguments --- src/qutip_qip/circuit/circuit.py | 6 +- src/qutip_qip/operations/controlled.py | 92 +++++++++++++++----------- src/qutip_qip/qasm.py | 6 +- src/qutip_qip/vqa.py | 2 +- 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 171b8459c..a95d8da58 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -247,12 +247,12 @@ def add_measurement( def add_gate( self, gate: Gate | str, - targets: int | Iterable[int] = [], - controls: int | Iterable[int] = [], + targets: int | Iterable[int] =(), + controls: int | Iterable[int] = (), arg_value: any = None, arg_label: str | None = None, control_value: None = None, - classical_controls: int | Iterable[int] = [], + classical_controls: int | Iterable[int] = (), classical_control_value: int | None = None, style: dict = None, index: None = None, diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 861437864..9ba1a9709 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -26,9 +26,9 @@ class ControlledGate(Gate): Examples: * If the gate should execute when the 0-th qubit is $|1\rangle$, - set ``control_value=1``. + set ``ctrl_value=1``. * If the gate should execute when two control qubits are $|10\rangle$ - (binary 10), set ``control_value=0b10``. + (binary 10), set ``ctrl_value=0b10``. """ __slots__ = ("arg_value", "arg_label", "_control_value") @@ -73,6 +73,9 @@ def __init_subclass__(cls, **kwargs) -> None: 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}" ) + # Check ctrl_value is valid + cls._validate_control_value() + # Automatically copy the validator from the target if cls.target_gate.is_parametric(): cls.validate_params = staticmethod(cls.target_gate.validate_params) @@ -90,22 +93,9 @@ def __init_subclass__(cls, **kwargs) -> None: if "latex_str" not in cls.__dict__: cls.latex_str = cls.target_gate.latex_str - def __init__( - self, - control_value: int | None = None, - arg_value: any = None, - arg_label: str | None = None, - ) -> None: - if control_value is not None: - self._validate_control_value(control_value) - self._control_value = control_value - else: - self._control_value = (2**self.num_ctrl_qubits) - 1 - + def __init__(self, arg_value: any = None, arg_label: str | None = None) -> None: if self.is_parametric(): - ParametricGate.__init__( - self, arg_value=arg_value, arg_label=arg_label - ) + ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) @property @abstractmethod @@ -116,12 +106,8 @@ def target_gate() -> Gate: def self_inverse(self) -> int: return self.target_gate.self_inverse - @property - def control_value(self) -> int: - return self._control_value - @classmethod - def _validate_control_value(cls, control_value: int) -> None: + def _validate_control_value(cls) -> None: """ Internal validation for the control value. @@ -134,15 +120,15 @@ def _validate_control_value(cls, control_value: int) -> None: possible for the number of control qubits ($2^N - 1$). """ - if type(control_value) is not int: + if type(cls.ctrl_value) is not int: raise TypeError( - f"Control value must be an int, got {control_value}" + f"Control value must be an int, got {cls.ctrl_value}" ) - if control_value < 0 or control_value > 2**cls.num_ctrl_qubits - 1: + if cls.ctrl_value < 0 or cls.ctrl_value > 2**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 {control_value}" + f"2^num_ctrl_qubits - 1, got {cls.ctrl_value}" ) def get_qobj(self) -> Qobj: @@ -166,21 +152,20 @@ def get_qobj(self) -> Qobj: def inverse_gate(self) -> Gate: if not self.is_parametric(): - inverse_gate = controlled( - self.target_gate.inverse_gate(), self.num_ctrl_qubits - ) - return inverse_gate(control_value=self.ctrl_value) + return controlled( + self.target_gate.inverse_gate(), + self.num_ctrl_qubits, + self.ctrl_value + )() else: inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() arg_value = inverse_target_gate.arg_value inverse_gate = controlled( - type(inverse_target_gate), self.num_ctrl_qubits + type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value ) - return inverse_gate( - control_value=self.ctrl_value, arg_value=arg_value - ) + return inverse_gate(arg_value=arg_value) @staticmethod def is_controlled() -> bool: @@ -194,13 +179,40 @@ def is_parametric(cls) -> bool: 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: - # if type(self) is not type(other): - # return False - # if self.control_value != other.control_value: - # return False - # return True +# class ControlledParamGate(ControlledGate, ParametricGate): +# def __init__(self, arg_value, arg_label: str | None = None) -> None: +# ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) + +# def __init_subclass__(cls, **kwargs) -> None: +# """ +# Validates the subclass definition. +# """ + +# super().__init_subclass__(**kwargs) +# if inspect.isabstract(cls): +# return + +# # Copy the num_params if not defined +# if "num_params" not in cls.__dict__: +# cls.num_params = cls.target_gate.num_params + +# def inverse_gate(self) -> Gate: +# inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() +# arg_value = inverse_target_gate.arg_value +# inverse_gate = controlled( +# type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value +# ) + +# return inverse_gate(arg_value=arg_value) + +# @property +# def self_inverse(self) -> int: +# return self.target_gate.self_inverse + +# @staticmethod +# def is_parametric() -> bool: +# return True def controlled( diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 1a7ec7340..e25918563 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -509,7 +509,7 @@ def _add_qiskit_gates( name, regs, args=None, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ @@ -686,7 +686,7 @@ def _add_predefined_gates( name, com_regs, com_args, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ @@ -742,7 +742,7 @@ def _gate_add( self, qc, command, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index ebc433812..cb8232bdc 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -495,7 +495,7 @@ class ParameterizedHamiltonian: Hamiltonian term which does not require parameters. """ - def __init__(self, parameterized_terms=[], constant_term=None): + def __init__(self, parameterized_terms=(), constant_term=None): self.p_terms = parameterized_terms self.c_term = constant_term self.num_parameters = len(parameterized_terms) From 64bb346cd5ea8735cf62059d3fd287639b4636fa Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 18:30:42 +0530 Subject: [PATCH 040/117] Added slots=True to dataclasses This is meant to ensure memory efficiency, faster lookup times. Since a QubitCircuit consists of several CircuitInstructions this will have a significant impact --- src/qutip_qip/circuit/draw/base_renderer.py | 4 ++-- src/qutip_qip/circuit/instruction.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/qutip_qip/circuit/draw/base_renderer.py b/src/qutip_qip/circuit/draw/base_renderer.py index 06304d40d..88649c799 100644 --- a/src/qutip_qip/circuit/draw/base_renderer.py +++ b/src/qutip_qip/circuit/draw/base_renderer.py @@ -6,7 +6,7 @@ from qutip_qip.circuit.draw.color_theme import qutip, light, dark, modern -@dataclass +@dataclass(slots=True) class StyleConfig: """ Dataclass to store the style configuration for circuit customization. @@ -83,6 +83,7 @@ class StyleConfig: label_pad: float = 0.1 bulge: str | bool = True align_layer: bool = False + measure_color = "#000000" theme: str | dict = "qutip" title: str | None = None bgcolor: str | None = None @@ -94,7 +95,6 @@ def __post_init__(self): if isinstance(self.bulge, bool): self.bulge = "round4" if self.bulge else "square" - self.measure_color = "#000000" if self.theme == "qutip": self.theme = qutip elif self.theme == "light": diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index c27991bb0..4ae2047cf 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -15,7 +15,7 @@ def _validate_non_negative_int_tuple(T: any, txt: str = ""): raise ValueError(f"{txt} indices must be non-negative, found {q}") -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class CircuitInstruction(ABC): operation: Gate | Measurement qubits: tuple[int] = tuple() @@ -56,13 +56,17 @@ def __repr__(self) -> str: return str(self) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class GateInstruction(CircuitInstruction): operation: Gate cbits_ctrl_value: int | None = None def __post_init__(self) -> None: - super().__post_init__() + super(GateInstruction, self).__post_init__() + # Don't make it super(), it will throw an error because slots=True + # destroys __class__ reference to the original class until Python 3.13 + # Check CPython Issue #90562, this has been resolved in Python 3.14 + if not ( isinstance(self.operation, Gate) or issubclass(self.operation, Gate) @@ -135,12 +139,12 @@ def __str__(self) -> str: cbits({self.cbits}), style({self.style})" -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class MeasurementInstruction(CircuitInstruction): operation: Measurement def __post_init__(self) -> None: - super().__post_init__() + super(MeasurementInstruction, self).__post_init__() if not isinstance(self.operation, Measurement): raise TypeError( f"Operation must be a measurement, got {self.operation}" From d1d15db4304bfeca6caaaf92868f5e010830e753 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 20:14:46 +0530 Subject: [PATCH 041/117] Add a ControlledParam Gate temporarily --- src/qutip_qip/operations/controlled.py | 53 +++++++++++++------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 9ba1a9709..54d89d953 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -31,7 +31,6 @@ class ControlledGate(Gate): (binary 10), set ``ctrl_value=0b10``. """ - __slots__ = ("arg_value", "arg_label", "_control_value") num_ctrl_qubits: int ctrl_value: int target_gate: Gate @@ -180,39 +179,39 @@ 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})" -# class ControlledParamGate(ControlledGate, ParametricGate): -# def __init__(self, arg_value, arg_label: str | None = None) -> None: -# ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) +class ControlledParamGate(ControlledGate, ParametricGate): + def __init__(self, arg_value, arg_label: str | None = None) -> None: + ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) -# def __init_subclass__(cls, **kwargs) -> None: -# """ -# Validates the subclass definition. -# """ + def __init_subclass__(cls, **kwargs) -> None: + """ + Validates the subclass definition. + """ -# super().__init_subclass__(**kwargs) -# if inspect.isabstract(cls): -# return + super().__init_subclass__(**kwargs) + if inspect.isabstract(cls): + return -# # Copy the num_params if not defined -# if "num_params" not in cls.__dict__: -# cls.num_params = cls.target_gate.num_params + # Copy the num_params if not defined + if "num_params" not in cls.__dict__: + cls.num_params = cls.target_gate.num_params -# def inverse_gate(self) -> Gate: -# inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() -# arg_value = inverse_target_gate.arg_value -# inverse_gate = controlled( -# type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value -# ) + def inverse_gate(self) -> Gate: + inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() + arg_value = inverse_target_gate.arg_value + inverse_gate = controlled( + type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value + ) -# return inverse_gate(arg_value=arg_value) + return inverse_gate(arg_value=arg_value) -# @property -# def self_inverse(self) -> int: -# return self.target_gate.self_inverse + @property + def self_inverse(self) -> int: + return self.target_gate.self_inverse -# @staticmethod -# def is_parametric() -> bool: -# return True + @staticmethod + def is_parametric() -> bool: + return True def controlled( From 348032ef4ef07d606c07e54361663d7247d20020 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 20:16:51 +0530 Subject: [PATCH 042/117] Remove standard gates from operations They should ideally be improted directy from /operation/std (or /operations/gates as we shall rename it to) --- src/qutip_qip/operations/__init__.py | 94 ---------------------------- 1 file changed, 94 deletions(-) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index ef142f0cc..3ddfbfa74 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -47,54 +47,6 @@ qubit_clifford_group, ) -# This should ideally be removed as it leads to duplication -from .std import ( - X, - Y, - Z, - RX, - RY, - RZ, - PHASE, - H, - SNOT, - SQRTNOT, - SQRTX, - SQRTXdag, - S, - Sdag, - T, - Tdag, - R, - QASMU, - SWAP, - ISWAP, - SQRTSWAP, - SQRTISWAP, - SWAPALPHA, - BERKELEY, - MS, - RZX, - CX, - CY, - CZ, - CRX, - CRY, - CRZ, - CS, - CT, - CH, - CNOT, - CPHASE, - CSIGN, - CQASMU, - TOFFOLI, - FREDKIN, - GLOBALPHASE, - IDLE, - GATE_CLASS_MAP, -) - __all__ = [ "Gate", "ParametricGate", @@ -139,50 +91,4 @@ "qubit_clifford_group", "expand_operator", "gate_sequence_product", - # The below ones need to be removed (should only be maintained in ./std), no duplication - "GATE_CLASS_MAP", - "GLOBALPHASE", - "X", - "Y", - "Z", - "RX", - "RY", - "RZ", - "PHASE", - "H", - "SNOT", - "SQRTNOT", - "SQRTX", - "SQRTXdag", - "S", - "Sdag", - "T", - "Tdag", - "R", - "QASMU", - "SWAP", - "ISWAP", - "CNOT", - "SQRTSWAP", - "SQRTISWAP", - "SWAPALPHA", - "MS", - "TOFFOLI", - "FREDKIN", - "BERKELEY", - "CNOT", - "CSIGN", - "CRX", - "CRY", - "CRZ", - "CY", - "CX", - "CZ", - "CH", - "CS", - "CT", - "CPHASE", - "RZX", - "CQASMU", - "IDLE", ] From 82a79ce13214e95b03db1378b0a546c893d296bf Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 20:23:23 +0530 Subject: [PATCH 043/117] Made some of the std gates as ControllledParamGate --- src/qutip_qip/operations/__init__.py | 3 ++- .../operations/std/two_qubit_gate.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 3ddfbfa74..e673b922b 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -9,7 +9,7 @@ ) from .gateclass import Gate, unitary_gate from .parametric import ParametricGate, AngleParametricGate -from .controlled import ControlledGate, controlled +from .controlled import ControlledGate, controlled, ControlledParamGate from .measurement import Measurement from .old_gates import ( rx, @@ -51,6 +51,7 @@ "Gate", "ParametricGate", "ControlledGate", + "ControlledParamGate", "unitary_gate", "controlled", "AngleParametricGate", diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 1b18f3ae3..9acddb388 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -4,7 +4,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate, ControlledParamGate from qutip_qip.operations.std import X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE @@ -29,6 +29,13 @@ class _ControlledTwoQubitGate(ControlledGate): ctrl_value: Final[int] = 1 +class _ControlledParametricTwoQubitGate(ControlledParamGate, AngleParametricGate): + __slots__ = () + num_qubits: Final[int] = 2 + num_ctrl_qubits: Final[int] = 1 + ctrl_value: Final[int] = 1 + + class SWAP(_TwoQubitGate): """ SWAP gate. @@ -716,7 +723,7 @@ class CS(_ControlledTwoQubitGate): latex_str = r"{\rm CS}" -class CPHASE(_ControlledTwoQubitGate): +class CPHASE(_ControlledParametricTwoQubitGate): r""" CPHASE gate. @@ -748,7 +755,7 @@ class CPHASE(_ControlledTwoQubitGate): latex_str = r"{\rm CPHASE}" -class CRX(_ControlledTwoQubitGate): +class CRX(_ControlledParametricTwoQubitGate): r""" Controlled X rotation. @@ -764,7 +771,7 @@ class CRX(_ControlledTwoQubitGate): latex_str = r"{\rm CRX}" -class CRY(_ControlledTwoQubitGate): +class CRY(_ControlledParametricTwoQubitGate): r""" Controlled Y rotation. @@ -780,7 +787,7 @@ class CRY(_ControlledTwoQubitGate): latex_str = r"{\rm CRY}" -class CRZ(_ControlledTwoQubitGate): +class CRZ(_ControlledParametricTwoQubitGate): r""" CRZ gate. @@ -812,7 +819,7 @@ class CRZ(_ControlledTwoQubitGate): latex_str = r"{\rm CRZ}" -class CQASMU(_ControlledTwoQubitGate): +class CQASMU(_ControlledParametricTwoQubitGate): r""" Controlled QASMU rotation. From db68bc15e843f44af94bfd6ded7f33de11a35f84 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 20:38:12 +0530 Subject: [PATCH 044/117] Made ControlledGate non-initialisable --- src/qutip_qip/circuit/circuit.py | 2 - src/qutip_qip/operations/controlled.py | 62 +++++++++++-------- .../operations/std/two_qubit_gate.py | 4 +- src/qutip_qip/operations/utils.py | 4 +- tests/test_gates.py | 58 ++++++++--------- tests/test_optpulseprocessor.py | 2 +- tests/test_qasm.py | 4 +- tests/test_qpe.py | 2 +- 8 files changed, 73 insertions(+), 65 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index a95d8da58..5cdf5f1c6 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -358,8 +358,6 @@ def add_gate( if gate_class.is_parametric(): gate = gate_class(arg_value=arg_value, arg_label=arg_label) - elif gate_class.is_controlled(): - gate = gate_class() else: gate = gate_class diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 54d89d953..f2bfc7bb6 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -75,14 +75,6 @@ def __init_subclass__(cls, **kwargs) -> None: # Check ctrl_value is valid cls._validate_control_value() - # Automatically copy the validator from the target - if cls.target_gate.is_parametric(): - cls.validate_params = staticmethod(cls.target_gate.validate_params) - - # Copy the num_params if not defined - if "num_params" not in cls.__dict__: - cls.num_params = cls.target_gate.num_params - # Default self_inverse if "self_inverse" not in cls.__dict__: cls.self_inverse = cls.target_gate.self_inverse @@ -92,10 +84,6 @@ def __init_subclass__(cls, **kwargs) -> None: if "latex_str" not in cls.__dict__: cls.latex_str = cls.target_gate.latex_str - def __init__(self, arg_value: any = None, arg_label: str | None = None) -> None: - if self.is_parametric(): - ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) - @property @abstractmethod def target_gate() -> Gate: @@ -130,7 +118,8 @@ def _validate_control_value(cls) -> None: f"2^num_ctrl_qubits - 1, got {cls.ctrl_value}" ) - def get_qobj(self) -> Qobj: + @classmethod + def get_qobj(cls) -> Qobj: """ Construct the full Qobj representation of the controlled gate. @@ -139,14 +128,14 @@ def get_qobj(self) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the controlled operation. """ - target_gate = self.target_gate - if self.is_parametric(): - target_gate = target_gate(self.arg_value) + target_gate = cls.target_gate + # if self.is_parametric(): + # target_gate = target_gate(self.arg_value) return controlled_gate_unitary( U=target_gate.get_qobj(), - num_controls=self.num_ctrl_qubits, - control_value=self.ctrl_value, + num_controls=cls.num_ctrl_qubits, + control_value=cls.ctrl_value, ) def inverse_gate(self) -> Gate: @@ -155,16 +144,16 @@ def inverse_gate(self) -> Gate: self.target_gate.inverse_gate(), self.num_ctrl_qubits, self.ctrl_value - )() - - else: - inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() - arg_value = inverse_target_gate.arg_value - inverse_gate = controlled( - type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value ) - return inverse_gate(arg_value=arg_value) + # else: + # inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() + # arg_value = inverse_target_gate.arg_value + # inverse_gate = controlled( + # type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value + # ) + + # return inverse_gate(arg_value=arg_value) @staticmethod def is_controlled() -> bool: @@ -192,10 +181,31 @@ def __init_subclass__(cls, **kwargs) -> None: if inspect.isabstract(cls): return + # Automatically copy the validator from the target + # if cls.target_gate.is_parametric(): + # cls.validate_params = staticmethod(cls.target_gate.validate_params) + # Copy the num_params if not defined if "num_params" not in cls.__dict__: cls.num_params = cls.target_gate.num_params + def get_qobj(self) -> Qobj: + """ + Construct the full Qobj representation of the controlled gate. + + Returns + ------- + qobj : qutip.Qobj + The unitary matrix representing the controlled operation. + """ + target_gate = self.target_gate(self.arg_value) + + return controlled_gate_unitary( + U=target_gate.get_qobj(), + num_controls=self.num_ctrl_qubits, + control_value=self.ctrl_value, + ) + def inverse_gate(self) -> Gate: inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() arg_value = inverse_target_gate.arg_value diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 9acddb388..af8b0d44d 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -599,7 +599,7 @@ class CY(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations.std import CY - >>> CY().get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> CY.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[ 1.+0j 0.+0j 0.+0j 0.+0j] @@ -622,7 +622,7 @@ class CZ(_ControlledTwoQubitGate): Examples -------- >>> from qutip_qip.operations.std import CZ - >>> CZ().get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> CZ.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = [[ 1. 0. 0. 0.] diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 195a16c03..7b0b31d56 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -130,7 +130,7 @@ def expand_operator( [1. 0. 0. 0. 0. 0.] [0. 1. 0. 0. 0. 0.] [0. 0. 1. 0. 0. 0.]] - >>> expand_operator(CX().get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE + >>> expand_operator(CX.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -141,7 +141,7 @@ def expand_operator( [0. 0. 0. 0. 0. 1. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 1.] [0. 0. 0. 0. 0. 0. 1. 0.]] - >>> expand_operator(CX().get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE + >>> expand_operator(CX.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] diff --git a/tests/test_gates.py b/tests/test_gates.py index dc1aff632..e06bada30 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -92,7 +92,7 @@ def test_swap(self): ) def test_toffoli(self, permutation): test = expand_operator( - std.TOFFOLI().get_qobj(), dims=[2] * 3, targets=permutation + std.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation ) base = qutip.tensor( 1 - qutip.basis([2, 2], [1, 1]).proj(), qutip.qeye(2) @@ -222,11 +222,11 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.CX(), 1, id="CX"), - pytest.param(std.CY(), 1, id="CY"), - pytest.param(std.CZ(), 1, id="CZ"), - pytest.param(std.CS(), 1, id="CS"), - pytest.param(std.CT(), 1, id="CT"), + pytest.param(std.CX, 1, id="CX"), + pytest.param(std.CY, 1, id="CY"), + pytest.param(std.CZ, 1, id="CZ"), + pytest.param(std.CS, 1, id="CS"), + pytest.param(std.CT, 1, id="CT"), pytest.param(std.SWAP, 0, id="SWAP"), pytest.param(std.ISWAP, 0, id="ISWAP"), pytest.param(std.SQRTSWAP, 0, id="SQRTSWAP"), @@ -252,9 +252,9 @@ def test_two_qubit(self, gate, n_controls): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.FREDKIN(), 1, id="Fredkin"), - pytest.param(std.TOFFOLI(), 2, id="Toffoli"), - pytest.param(RandomThreeQubitGate(), 2, id="random"), + pytest.param(std.FREDKIN, 1, id="Fredkin"), + pytest.param(std.TOFFOLI, 2, id="Toffoli"), + pytest.param(RandomThreeQubitGate, 2, id="random"), ], ) def test_three_qubit(self, gate: Gate, n_controls): @@ -316,7 +316,7 @@ def test_general_qubit_expansion(self, n_targets): def test_cnot_explicit(self): test = expand_operator( - std.CX().get_qobj(), dims=[2] * 3, targets=[2, 0] + std.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] ).full() expected = np.array( [ @@ -376,11 +376,11 @@ def test_non_qubit_systems(self, dimensions): def test_dtype(self): expanded_qobj = expand_operator( - std.CX().get_qobj(), dims=[2, 2, 2] + std.CX.get_qobj(), dims=[2, 2, 2] ).data assert isinstance(expanded_qobj, qutip.data.CSR) expanded_qobj = expand_operator( - std.CX().get_qobj(), dims=[2, 2, 2], dtype="dense" + std.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" ).data assert isinstance(expanded_qobj, qutip.data.Dense) @@ -485,28 +485,28 @@ def test_gates_class(): ] CONTROLLED_GATE = [ - std.CX(), - std.CY(), - std.CZ(), - std.CH(), - std.CS(), - std.CT(), + std.CX, + std.CY, + std.CZ, + std.CH, + std.CS, + std.CT, std.CRX(arg_value=0.7), std.CRY(arg_value=0.88), std.CRZ(arg_value=0.78), std.CPHASE(arg_value=0.9), std.CQASMU(arg_value=[0.9, 0.22, 0.15]), - std.TOFFOLI(), - std.FREDKIN(), + std.TOFFOLI, + std.FREDKIN, ] -@pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) -def test_gate_inverse(gate: Gate): - n = 2**gate.num_qubits - inverse_gate = gate.inverse_gate() - np.testing.assert_allclose( - (gate.get_qobj() * inverse_gate.get_qobj()).full(), - np.eye(n), - atol=1e-12, - ) +# @pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) +# def test_gate_inverse(gate: Gate): +# n = 2**gate.num_qubits +# inverse_gate = gate.inverse_gate() +# np.testing.assert_allclose( +# (gate.get_qobj() * inverse_gate.get_qobj()).full(), +# np.eye(n), +# atol=1e-12, +# ) diff --git a/tests/test_optpulseprocessor.py b/tests/test_optpulseprocessor.py index 486bae91b..651ca20d4 100644 --- a/tests/test_optpulseprocessor.py +++ b/tests/test_optpulseprocessor.py @@ -64,7 +64,7 @@ def test_multi_qubits(self): test.add_control(sigmay(), cyclic_permutation=True) # test pulse genration for cnot gate, with kwargs - qc = [tensor([identity(2), CX().get_qobj()])] + qc = [tensor([identity(2), CX.get_qobj()])] test.load_circuit( qc, num_tslots=num_tslots, evo_time=evo_time, min_fid_err=1.0e-6 ) diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 52641aac6..b35f20267 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -86,7 +86,7 @@ def test_custom_gates(): qc = read_qasm(filepath) unitaries = qc.propagators() assert (unitaries[0] - unitaries[1]).norm() < 1e-12 - ry_cx = std.CX().get_qobj() * tensor( + ry_cx = std.CX.get_qobj() * tensor( identity(2), std.RY(np.pi / 2).get_qobj() ) assert (unitaries[2] - ry_cx).norm() < 1e-12 @@ -140,7 +140,7 @@ def test_export_import(): qc.add_gate(std.CRY(arg_value=np.pi), targets=1, controls=0) qc.add_gate(std.CRX(arg_value=np.pi), targets=1, controls=0) qc.add_gate(std.CRZ(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(std.CX(), targets=1, controls=0) + qc.add_gate(std.CX, targets=1, controls=0) qc.add_gate(std.TOFFOLI, targets=2, controls=[0, 1]) # qc.add_gate(SQRTX, targets=0) qc.add_gate(std.CS, targets=1, controls=0) diff --git a/tests/test_qpe.py b/tests/test_qpe.py index cdd561d90..96e91e692 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -30,7 +30,7 @@ def test_controlled_unitary(self): controlled_u = controlled( gate=unitary_gate(gate_name="CU", U=U), - )() + ) assert_equal(controlled_u.ctrl_value, 1) assert_((controlled_u.target_gate.get_qobj() - U).norm() < 1e-12) From b19272af2eddefb46a21fcfe4212e6c6363cbc82 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 21:25:16 +0530 Subject: [PATCH 045/117] Added Dynamic Descriptors, Delegation to ControlledGate This is meant to allow simple API interface (overhead is negligible which is only incured during circuit construction). This should allow both CX.get_qobj() and CRX(0.5).get_qobj() --- src/qutip_qip/operations/controlled.py | 56 +++++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index f2bfc7bb6..3841ccf90 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -1,10 +1,27 @@ import inspect +from typing import Type +from functools import partial from abc import abstractmethod from qutip import Qobj from qutip_qip.operations import Gate, ParametricGate, 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): + if instance is None: + return partial(self.func, owner) # Called on the class (e.g., CX.get_qobj()) + + return partial(self.func, instance) # Called on the instance (e.g., CRX(0.5).get_qobj()) + + class ControlledGate(Gate): r""" Abstract base class for controlled quantum gates. @@ -33,7 +50,7 @@ class ControlledGate(Gate): num_ctrl_qubits: int ctrl_value: int - target_gate: Gate + target_gate: Type[Gate] def __init_subclass__(cls, **kwargs) -> None: """ @@ -72,9 +89,11 @@ def __init_subclass__(cls, **kwargs) -> None: 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}" ) - # Check ctrl_value is valid cls._validate_control_value() + if hasattr(cls.target_gate, "num_params") and "num_params" not in cls.__dict__: + cls.num_params = cls.target_gate.num_params + # Default self_inverse if "self_inverse" not in cls.__dict__: cls.self_inverse = cls.target_gate.self_inverse @@ -84,11 +103,22 @@ def __init_subclass__(cls, **kwargs) -> None: if "latex_str" not in cls.__dict__: cls.latex_str = cls.target_gate.latex_str + def __init__(self, *args, **kwargs) -> None: + self._target_inst = self.target_gate(*args, **kwargs) + @property @abstractmethod def target_gate() -> Gate: pass + 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) + @property def self_inverse(self) -> int: return self.target_gate.self_inverse @@ -118,8 +148,8 @@ def _validate_control_value(cls) -> None: f"2^num_ctrl_qubits - 1, got {cls.ctrl_value}" ) - @classmethod - def get_qobj(cls) -> Qobj: + @class_or_instance_method + def get_qobj(self_or_cls) -> Qobj: """ Construct the full Qobj representation of the controlled gate. @@ -128,16 +158,20 @@ def get_qobj(cls) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the controlled operation. """ - target_gate = cls.target_gate - # if self.is_parametric(): - # target_gate = target_gate(self.arg_value) - + if isinstance(self_or_cls, type): + return controlled_gate_unitary( + U=self_or_cls.target_gate.get_qobj(), + num_controls=self_or_cls.num_ctrl_qubits, + control_value=self_or_cls.ctrl_value, + ) + return controlled_gate_unitary( - U=target_gate.get_qobj(), - num_controls=cls.num_ctrl_qubits, - control_value=cls.ctrl_value, + U=self_or_cls._target_inst.get_qobj(), + num_controls=self_or_cls.num_ctrl_qubits, + control_value=self_or_cls.ctrl_value, ) + def inverse_gate(self) -> Gate: if not self.is_parametric(): return controlled( From 5130d2519f93c1c7ae56e1f060e87358207c108e Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 21:33:54 +0530 Subject: [PATCH 046/117] Deleted ControlledParamGate Included that functionality within ControlledGate as intended using Delegations, Dynamic Descriptor --- src/qutip_qip/operations/__init__.py | 3 +- src/qutip_qip/operations/controlled.py | 71 ++----------------- .../operations/std/two_qubit_gate.py | 20 ++---- 3 files changed, 13 insertions(+), 81 deletions(-) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index e673b922b..3ddfbfa74 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -9,7 +9,7 @@ ) from .gateclass import Gate, unitary_gate from .parametric import ParametricGate, AngleParametricGate -from .controlled import ControlledGate, controlled, ControlledParamGate +from .controlled import ControlledGate, controlled from .measurement import Measurement from .old_gates import ( rx, @@ -51,7 +51,6 @@ "Gate", "ParametricGate", "ControlledGate", - "ControlledParamGate", "unitary_gate", "controlled", "AngleParametricGate", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 3841ccf90..f3a69d0b0 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -4,7 +4,7 @@ from abc import abstractmethod from qutip import Qobj -from qutip_qip.operations import Gate, ParametricGate, controlled_gate_unitary +from qutip_qip.operations import Gate, controlled_gate_unitary class class_or_instance_method: @@ -91,9 +91,6 @@ def __init_subclass__(cls, **kwargs) -> None: cls._validate_control_value() - if hasattr(cls.target_gate, "num_params") and "num_params" not in cls.__dict__: - cls.num_params = cls.target_gate.num_params - # Default self_inverse if "self_inverse" not in cls.__dict__: cls.self_inverse = cls.target_gate.self_inverse @@ -106,11 +103,6 @@ def __init_subclass__(cls, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None: self._target_inst = self.target_gate(*args, **kwargs) - @property - @abstractmethod - def target_gate() -> Gate: - pass - def __getattr__(self, name: str) -> any: """ If an attribute (like 'arg_value') or method (like 'validate_params') @@ -119,6 +111,11 @@ def __getattr__(self, name: str) -> any: """ return getattr(self._target_inst, name) + @property + @abstractmethod + def target_gate() -> Gate: + pass + @property def self_inverse(self) -> int: return self.target_gate.self_inverse @@ -202,62 +199,6 @@ 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})" -class ControlledParamGate(ControlledGate, ParametricGate): - def __init__(self, arg_value, arg_label: str | None = None) -> None: - ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) - - def __init_subclass__(cls, **kwargs) -> None: - """ - Validates the subclass definition. - """ - - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): - return - - # Automatically copy the validator from the target - # if cls.target_gate.is_parametric(): - # cls.validate_params = staticmethod(cls.target_gate.validate_params) - - # Copy the num_params if not defined - if "num_params" not in cls.__dict__: - cls.num_params = cls.target_gate.num_params - - def get_qobj(self) -> Qobj: - """ - Construct the full Qobj representation of the controlled gate. - - Returns - ------- - qobj : qutip.Qobj - The unitary matrix representing the controlled operation. - """ - target_gate = self.target_gate(self.arg_value) - - return controlled_gate_unitary( - U=target_gate.get_qobj(), - num_controls=self.num_ctrl_qubits, - control_value=self.ctrl_value, - ) - - def inverse_gate(self) -> Gate: - inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() - arg_value = inverse_target_gate.arg_value - inverse_gate = controlled( - type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value - ) - - return inverse_gate(arg_value=arg_value) - - @property - def self_inverse(self) -> int: - return self.target_gate.self_inverse - - @staticmethod - def is_parametric() -> bool: - return True - - def controlled( gate: Gate, n_ctrl_qubits: int = 1, diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index af8b0d44d..1d52e2c85 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -4,7 +4,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate, ControlledParamGate +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate from qutip_qip.operations.std import X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE @@ -28,14 +28,6 @@ class _ControlledTwoQubitGate(ControlledGate): num_ctrl_qubits: Final[int] = 1 ctrl_value: Final[int] = 1 - -class _ControlledParametricTwoQubitGate(ControlledParamGate, AngleParametricGate): - __slots__ = () - num_qubits: Final[int] = 2 - num_ctrl_qubits: Final[int] = 1 - ctrl_value: Final[int] = 1 - - class SWAP(_TwoQubitGate): """ SWAP gate. @@ -723,7 +715,7 @@ class CS(_ControlledTwoQubitGate): latex_str = r"{\rm CS}" -class CPHASE(_ControlledParametricTwoQubitGate): +class CPHASE(_ControlledTwoQubitGate): r""" CPHASE gate. @@ -755,7 +747,7 @@ class CPHASE(_ControlledParametricTwoQubitGate): latex_str = r"{\rm CPHASE}" -class CRX(_ControlledParametricTwoQubitGate): +class CRX(_ControlledTwoQubitGate): r""" Controlled X rotation. @@ -771,7 +763,7 @@ class CRX(_ControlledParametricTwoQubitGate): latex_str = r"{\rm CRX}" -class CRY(_ControlledParametricTwoQubitGate): +class CRY(_ControlledTwoQubitGate): r""" Controlled Y rotation. @@ -787,7 +779,7 @@ class CRY(_ControlledParametricTwoQubitGate): latex_str = r"{\rm CRY}" -class CRZ(_ControlledParametricTwoQubitGate): +class CRZ(_ControlledTwoQubitGate): r""" CRZ gate. @@ -819,7 +811,7 @@ class CRZ(_ControlledParametricTwoQubitGate): latex_str = r"{\rm CRZ}" -class CQASMU(_ControlledParametricTwoQubitGate): +class CQASMU(_ControlledTwoQubitGate): r""" Controlled QASMU rotation. From 77bc5eb9acac465cdc9e3819df8f19c23affe882 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 28 Feb 2026 21:52:24 +0530 Subject: [PATCH 047/117] Resolved failing inverse gate tests --- src/qutip_qip/operations/controlled.py | 44 +++++++++++++------------- tests/test_gates.py | 18 +++++------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index f3a69d0b0..d5aee8086 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -146,7 +146,7 @@ def _validate_control_value(cls) -> None: ) @class_or_instance_method - def get_qobj(self_or_cls) -> Qobj: + def get_qobj(cls_or_self) -> Qobj: """ Construct the full Qobj representation of the controlled gate. @@ -155,36 +155,36 @@ def get_qobj(self_or_cls) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the controlled operation. """ - if isinstance(self_or_cls, type): + if isinstance(cls_or_self, type): return controlled_gate_unitary( - U=self_or_cls.target_gate.get_qobj(), - num_controls=self_or_cls.num_ctrl_qubits, - control_value=self_or_cls.ctrl_value, + U=cls_or_self.target_gate.get_qobj(), + num_controls=cls_or_self.num_ctrl_qubits, + control_value=cls_or_self.ctrl_value, ) return controlled_gate_unitary( - U=self_or_cls._target_inst.get_qobj(), - num_controls=self_or_cls.num_ctrl_qubits, - control_value=self_or_cls.ctrl_value, + U=cls_or_self._target_inst.get_qobj(), + num_controls=cls_or_self.num_ctrl_qubits, + control_value=cls_or_self.ctrl_value, ) - - def inverse_gate(self) -> Gate: - if not self.is_parametric(): + @class_or_instance_method + def inverse_gate(cls_or_self) -> Gate: + if isinstance(cls_or_self, type): return controlled( - self.target_gate.inverse_gate(), - self.num_ctrl_qubits, - self.ctrl_value + cls_or_self.target_gate.inverse_gate(), + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value ) - # else: - # inverse_target_gate = self.target_gate(self.arg_value).inverse_gate() - # arg_value = inverse_target_gate.arg_value - # inverse_gate = controlled( - # type(inverse_target_gate), self.num_ctrl_qubits, self.ctrl_value - # ) - - # return inverse_gate(arg_value=arg_value) + inverse_target_gate = cls_or_self._target_inst.inverse_gate() + arg_value = inverse_target_gate.arg_value + inverse_gate = controlled( + type(inverse_target_gate), + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value + ) + return inverse_gate(arg_value=arg_value) @staticmethod def is_controlled() -> bool: diff --git a/tests/test_gates.py b/tests/test_gates.py index e06bada30..336439c09 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -501,12 +501,12 @@ def test_gates_class(): ] -# @pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) -# def test_gate_inverse(gate: Gate): -# n = 2**gate.num_qubits -# inverse_gate = gate.inverse_gate() -# np.testing.assert_allclose( -# (gate.get_qobj() * inverse_gate.get_qobj()).full(), -# np.eye(n), -# atol=1e-12, -# ) +@pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) +def test_gate_inverse(gate: Gate): + n = 2**gate.num_qubits + inverse_gate = gate.inverse_gate() + np.testing.assert_allclose( + (gate.get_qobj() * inverse_gate.get_qobj()).full(), + np.eye(n), + atol=1e-12, + ) From a0019ddb14883be7aba3a285e490ee701928ff21 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 01:13:58 +0530 Subject: [PATCH 048/117] Corrected Metaclass for Gates Added __setattr__ for ControlledGate, in _GateMetaClass added a _is_frozen class attribute for signalling (required to prevent overwrite of default arguments), moved __str__, __repr__ there, removed __call__ as there is no longer need of it --- src/qutip_qip/operations/controlled.py | 20 ++++++-- src/qutip_qip/operations/gateclass.py | 66 ++++++++++++-------------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index d5aee8086..a0890ecc1 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -31,12 +31,12 @@ class ControlledGate(Gate): Attributes ---------- - num_ctrl_qubits : int - The number of qubits acting as controls. - target_gate : 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. @@ -47,6 +47,7 @@ class ControlledGate(Gate): * If the gate should execute when two control qubits are $|10\rangle$ (binary 10), set ``ctrl_value=0b10``. """ + __slots__ = ('_target_inst') num_ctrl_qubits: int ctrl_value: int @@ -88,10 +89,10 @@ def __init_subclass__(cls, **kwargs) -> None: 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() # 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 @@ -111,6 +112,17 @@ def __getattr__(self, name: str) -> any: """ 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) + @property @abstractmethod def target_gate() -> Gate: diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 872c2aa5a..49fa492a9 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -14,6 +14,7 @@ class _GateMetaClass(ABCMeta): "num_qubits", "num_ctrl_qubits", "num_params", + "ctrl_value", "self_inverse", "is_clifford", "target_gate", @@ -36,6 +37,7 @@ def __init__(cls, name, bases, attrs): # Don't register the Abstract Gate Classes or private helpers if inspect.isabstract(cls) or name.startswith("_"): + cls._is_frozen = True return namespace = attrs.get("namespace", "std") @@ -50,33 +52,10 @@ def __init__(cls, name, bases, attrs): ) cls._registry[namespace].add(name) - def clear_cache(cls, namespace: str): - """ - Clears the gate class registry based on the namespace. - - Parameters - ---------- - namespace : str, optional - If provided, only clears gates belonging to this namespace - (e.g., 'custom'). If None, clears ALL gates (useful for hard resets). - """ - if namespace == "std": - raise ValueError("Can't clear std Gates") - else: - cls._registry[namespace] = set() - - def __call__(cls, *args, **kwargs): - """ - So creating CNOT(control_value=1) 10,000 times (e.g., for a large circuit) becomes instant - because it's just a dictionary lookup after the first time. Also in memory - we only need to store one copy of CNOT gate, no matter how many times it appears - in the circuit. - """ - return super().__call__(*args, **kwargs) + # This class attribute (flag) signals class (or subclass) is built, + # don't overwrite any defaults like num_qubits etc in __setattr__. + cls._is_frozen = True - # For RX(0.5), RX(0.1) we want different instances. - # Same for CX(control_value=0), CX(control_value=1) - # TODO This needs to be implemented efficiently def __setattr__(cls, name: str, value: any) -> None: """ @@ -93,14 +72,35 @@ class X(Gate): This is required since num_qubits etc. are class attributes (shared by all object instances). """ - # Check if the attribute is in our protected set - # Using cls.__dict__ ignores parent classes, allowing __init_subclass__ - # to set it once for the child class. - if name in cls._read_only_set and name in cls.__dict__: + # 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 cls._read_only_set: raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) + def __str__(self) -> str: + return f"Gate({self.name})" + + def __repr__(self) -> str: + return f"Gate({self.name}, num_qubits={self.num_qubits})" + + def clear_cache(cls, namespace: str): + """ + Clears the gate class registry based on the namespace. + + Parameters + ---------- + namespace : str, optional + If provided, only clears gates belonging to this namespace + (e.g., 'custom'). If None, clears ALL gates (useful for hard resets). + """ + if namespace == "std": + raise ValueError("Can't clear std Gates") + else: + cls._registry[namespace] = set() + + class Gate(ABC, metaclass=_GateMetaClass): r""" Abstract base class for a quantum gate. @@ -250,12 +250,6 @@ def is_parametric() -> bool: """ return False - def __str__(self) -> str: - return f"Gate({self.name})" - - def __repr__(self) -> str: - return f"Gate({self.name}, num_qubits={self.num_qubits}, qobj={self.get_qobj()})" - def unitary_gate(gate_name: str, U: Qobj, namespace: str = "custom") -> Gate: """ From 10a351c8479069dc5f8341a8075f2423d2cf30da Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 06:54:46 +0530 Subject: [PATCH 049/117] Move hadamard_transform to utils --- src/qutip_qip/operations/__init__.py | 2 +- src/qutip_qip/operations/old_gates.py | 21 ++++++--------------- src/qutip_qip/operations/utils.py | 14 +++++++++++++- tests/test_gates.py | 3 +-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 3ddfbfa74..c07e15392 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -3,6 +3,7 @@ """ from .utils import ( + hadamard_transform, expand_operator, gate_sequence_product, controlled_gate_unitary, @@ -43,7 +44,6 @@ toffoli, rotation, globalphase, - hadamard_transform, qubit_clifford_group, ) diff --git a/src/qutip_qip/operations/old_gates.py b/src/qutip_qip/operations/old_gates.py index 87b8f6db5..f84d918cf 100644 --- a/src/qutip_qip/operations/old_gates.py +++ b/src/qutip_qip/operations/old_gates.py @@ -1073,21 +1073,6 @@ def globalphase(theta, N=1): # -def hadamard_transform(N=1): - """Quantum object representing the N-qubit Hadamard gate. - - Returns - ------- - q : qobj - Quantum object representation of the N-qubit Hadamard gate. - - """ - data = [[1, 1], [1, -1]] - H = Qobj(data) / np.sqrt(2) - - return tensor([H] * N) - - def _powers(op, N): """ Generator that yields powers of an operator `op`, @@ -1125,6 +1110,12 @@ def qubit_clifford_group(N=None, target=0): """ + warnings.warn( + "qubit_clifford has been deprecated and will be removed in future version.", + DeprecationWarning, + stacklevel=2 + ) + # The Ross-Selinger presentation of the single-qubit Clifford # group expresses each element in the form C_{ijk} = E^i X^j S^k # for gates E, X and S, and for i in range(3), j in range(2) and diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 7b0b31d56..bf308f364 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -85,7 +85,6 @@ def _targets_to_list( raise ValueError("Targets must be smaller than N={}.".format(N)) return targets - def expand_operator( oper: Qobj, dims: Iterable[int], @@ -182,6 +181,19 @@ def expand_operator( out = tensor([oper] + id_list).permute(new_order) return out.to(dtype) +def hadamard_transform(N=1): + """Quantum object representing the N-qubit Hadamard gate. + + Returns + ------- + q : qobj + Quantum object representation of the N-qubit Hadamard gate. + + """ + data = [[1, 1], [1, -1]] + H = Qobj(data) / np.sqrt(2) + + return tensor([H] * N) def gate_sequence_product( U_list: list[Qobj], diff --git a/tests/test_gates.py b/tests/test_gates.py index 336439c09..a3b37eec6 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -3,10 +3,9 @@ import itertools import numpy as np import qutip -from qutip.core.gates import hadamard_transform from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ( - Gate, expand_operator, controlled, unitary_gate, qubit_clifford_group + Gate, expand_operator, controlled, unitary_gate, qubit_clifford_group, hadamard_transform ) import qutip_qip.operations.std as std From 096573de1f02d5f2337117233ce8e253e8dfb5b8 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 06:57:09 +0530 Subject: [PATCH 050/117] Replaced inverse_gate with inverse in Gate class --- src/qutip_qip/operations/controlled.py | 10 ++++---- src/qutip_qip/operations/gateclass.py | 2 +- .../operations/std/single_qubit_gate.py | 24 +++++++++---------- .../operations/std/two_qubit_gate.py | 22 ++++++++--------- tests/test_gates.py | 4 ++-- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index a0890ecc1..16041a227 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -181,22 +181,22 @@ def get_qobj(cls_or_self) -> Qobj: ) @class_or_instance_method - def inverse_gate(cls_or_self) -> Gate: + def inverse(cls_or_self) -> Gate: if isinstance(cls_or_self, type): return controlled( - cls_or_self.target_gate.inverse_gate(), + cls_or_self.target_gate.inverse(), cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value ) - inverse_target_gate = cls_or_self._target_inst.inverse_gate() + inverse_target_gate = cls_or_self._target_inst.inverse() arg_value = inverse_target_gate.arg_value - inverse_gate = controlled( + inverse = controlled( type(inverse_target_gate), cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value ) - return inverse_gate(arg_value=arg_value) + return inverse(arg_value=arg_value) @staticmethod def is_controlled() -> bool: diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 49fa492a9..923558e87 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -211,7 +211,7 @@ def get_qobj() -> Qobj: pass @classmethod - def inverse_gate(cls) -> Gate: + def inverse(cls) -> Gate: """ Return the inverse of the gate. diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 856e198b8..67aef1d7d 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -181,7 +181,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTXdag @@ -210,7 +210,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTX @@ -252,7 +252,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, 1j]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return Sdag @@ -281,7 +281,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, -1j]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return S @@ -309,7 +309,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return Tdag @@ -337,7 +337,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return T @@ -370,7 +370,7 @@ def get_qobj(self) -> Qobj: dims=[[2], [2]], ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta = self.arg_value[0] return RX(-theta) @@ -403,7 +403,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta = self.arg_value[0] return RY(-theta) @@ -431,7 +431,7 @@ def get_qobj(self) -> Qobj: phi = self.arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta = self.arg_value[0] return RZ(-theta) @@ -459,7 +459,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: phi = self.arg_value[0] return PHASE(-phi) @@ -505,7 +505,7 @@ def get_qobj(self) -> Qobj: ] ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: phi, theta = self.arg_value return R([phi, -theta]) @@ -547,6 +547,6 @@ def get_qobj(self) -> Qobj: ] ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta, phi, gamma = self.arg_value return QASMU([-theta, -gamma, -phi]) diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 1d52e2c85..15fee073d 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -88,7 +88,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return ISWAPdag @@ -122,7 +122,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return ISWAP @@ -162,7 +162,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTSWAPdag @@ -202,7 +202,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTSWAP @@ -243,7 +243,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTISWAPdag @@ -284,7 +284,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return SQRTISWAP @@ -331,7 +331,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return BERKELEYdag @@ -378,7 +378,7 @@ def get_qobj(): ) @staticmethod - def inverse_gate() -> Gate: + def inverse() -> Gate: return BERKELEY @@ -435,7 +435,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: alpha = self.arg_value[0] return SWAPALPHA(-alpha) @@ -493,7 +493,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta, phi = self.arg_value return MS([-theta, phi]) @@ -543,7 +543,7 @@ def get_qobj(self): dims=[[2, 2], [2, 2]], ) - def inverse_gate(self) -> Gate: + def inverse(self) -> Gate: theta = self.arg_value[0] return RZX(-theta) diff --git a/tests/test_gates.py b/tests/test_gates.py index a3b37eec6..749aa2c92 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -503,9 +503,9 @@ def test_gates_class(): @pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) def test_gate_inverse(gate: Gate): n = 2**gate.num_qubits - inverse_gate = gate.inverse_gate() + inverse = gate.inverse() np.testing.assert_allclose( - (gate.get_qobj() * inverse_gate.get_qobj()).full(), + (gate.get_qobj() * inverse.get_qobj()).full(), np.eye(n), atol=1e-12, ) From bebc64b78ea6802bb3b7e4bb84001874bcf8ed0e Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 14:46:52 +0530 Subject: [PATCH 051/117] Add static method get_qobj, cache For the standard Controlled Gates which are not parametrized added the static method get_qobj back explicitly to prevent the overhead because of controlled_gate_unitary method. Also @cache the non-parametrized standard gates --- src/qutip_qip/operations/std/other_gates.py | 38 ++++++++++- .../operations/std/single_qubit_gate.py | 12 ++++ .../operations/std/two_qubit_gate.py | 63 +++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/std/other_gates.py index f6fb29864..5c85fd644 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/std/other_gates.py @@ -1,7 +1,9 @@ +from functools import cache + import scipy.sparse as sp import numpy as np - from qutip import Qobj + from qutip_qip.operations import ControlledGate, AngleParametricGate from qutip_qip.operations.std import X, SWAP @@ -67,6 +69,23 @@ class TOFFOLI(ControlledGate): target_gate = X latex_str = r"{\rm TOFFOLI}" + @staticmethod + @cache + def get_qobj() -> Qobj: + return Qobj( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ], + dims=[[2, 2, 2], [2, 2, 2]], + ) + class FREDKIN(ControlledGate): """ @@ -95,3 +114,20 @@ class FREDKIN(ControlledGate): ctrl_value = 1 target_gate = SWAP latex_str = r"{\rm FREDKIN}" + + @staticmethod + @cache + def get_qobj() -> Qobj: + return Qobj( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ], + dims=[[2, 2, 2], [2, 2, 2]], + ) diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py index 67aef1d7d..eeb1a5755 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/std/single_qubit_gate.py @@ -1,3 +1,4 @@ +from functools import cache import warnings import numpy as np @@ -40,6 +41,7 @@ class X(_SingleQubitGate): latex_str = r"X" @staticmethod + @cache def get_qobj() -> Qobj: return sigmax(dtype="dense") @@ -65,6 +67,7 @@ class Y(_SingleQubitGate): latex_str = r"Y" @staticmethod + @cache def get_qobj() -> Qobj: return sigmay(dtype="dense") @@ -90,6 +93,7 @@ class Z(_SingleQubitGate): latex_str = r"Z" @staticmethod + @cache def get_qobj() -> Qobj: return sigmaz(dtype="dense") @@ -110,6 +114,7 @@ class IDLE(_SingleQubitGate): latex_str = r"{\rm IDLE}" @staticmethod + @cache def get_qobj() -> Qobj: return qeye(2) @@ -135,6 +140,7 @@ class H(_SingleQubitGate): latex_str = r"H" @staticmethod + @cache def get_qobj() -> Qobj: return 1 / np.sqrt(2.0) * Qobj([[1, 1], [1, -1]]) @@ -177,6 +183,7 @@ class SQRTX(_SingleQubitGate): latex_str = r"\sqrt{\rm X}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) @@ -206,6 +213,7 @@ class SQRTXdag(_SingleQubitGate): latex_str = r"\sqrt{\rm X}^\dagger" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) @@ -248,6 +256,7 @@ class S(_SingleQubitGate): latex_str = r"{\rm S}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[1, 0], [0, 1j]]) @@ -277,6 +286,7 @@ class Sdag(_SingleQubitGate): latex_str = r"{\rm S^\dagger}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[1, 0], [0, -1j]]) @@ -305,6 +315,7 @@ class T(_SingleQubitGate): latex_str = r"{\rm T}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) @@ -333,6 +344,7 @@ class Tdag(_SingleQubitGate): latex_str = r"{\rm Tdag}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py index 15fee073d..9e0abe4be 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/std/two_qubit_gate.py @@ -1,4 +1,5 @@ from typing import Final +from functools import cache import warnings import numpy as np @@ -28,6 +29,7 @@ class _ControlledTwoQubitGate(ControlledGate): num_ctrl_qubits: Final[int] = 1 ctrl_value: Final[int] = 1 + class SWAP(_TwoQubitGate): """ SWAP gate. @@ -570,6 +572,14 @@ class CX(_ControlledTwoQubitGate): is_clifford = True latex_str = r"{\rm CNOT}" + @staticmethod + @cache + def get_qobj() -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], + dims=[[2, 2], [2, 2]], + ) + class CNOT(CX): __slots__ = () @@ -606,6 +616,14 @@ class CY(_ControlledTwoQubitGate): target_gate = Y latex_str = r"{\rm CY}" + @staticmethod + @cache + def get_qobj(): + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], + dims=[[2, 2], [2, 2]], + ) + class CZ(_ControlledTwoQubitGate): """ @@ -629,6 +647,14 @@ class CZ(_ControlledTwoQubitGate): is_clifford = True latex_str = r"{\rm CZ}" + @staticmethod + @cache + def get_qobj(): + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], + dims=[[2, 2], [2, 2]], + ) + class CSIGN(CZ): __slots__ = () @@ -666,6 +692,20 @@ class CH(_ControlledTwoQubitGate): target_gate = H latex_str = r"{\rm CH}" + @staticmethod + @cache + def get_qobj(): + sq_2 = 1 / np.sqrt(2) + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, sq_2, sq_2], + [0, 0, sq_2, -sq_2], + ], + dims=[[2, 2], [2, 2]], + ) + class CT(_ControlledTwoQubitGate): r""" @@ -690,6 +730,19 @@ class CT(_ControlledTwoQubitGate): target_gate = T latex_str = r"{\rm CT}" + @staticmethod + @cache + def get_qobj(): + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, (1 + 1j) / np.sqrt(2)], + ], + dims=[[2, 2], [2, 2]], + ) + class CS(_ControlledTwoQubitGate): r""" @@ -714,6 +767,16 @@ class CS(_ControlledTwoQubitGate): target_gate = S latex_str = r"{\rm CS}" + @staticmethod + @cache + def get_qobj(): + return Qobj( + np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] + ), + dims=[[2, 2], [2, 2]], + ) + class CPHASE(_ControlledTwoQubitGate): r""" From 9b6f022b7f08d74cf386c21194f017dafb91b6b5 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 14:58:42 +0530 Subject: [PATCH 052/117] Renamed std to gates --- src/qutip_qip/algorithms/bit_flip.py | 2 +- src/qutip_qip/algorithms/phase_flip.py | 2 +- src/qutip_qip/algorithms/qft.py | 2 +- src/qutip_qip/algorithms/qpe.py | 2 +- src/qutip_qip/circuit/_decompose.py | 2 +- src/qutip_qip/circuit/circuit.py | 13 +- src/qutip_qip/compiler/cavityqedcompiler.py | 2 +- src/qutip_qip/compiler/circuitqedcompiler.py | 4 +- src/qutip_qip/compiler/scheduler.py | 2 +- .../decompose/decompose_single_qubit_gate.py | 2 +- src/qutip_qip/device/circuitqed.py | 2 +- src/qutip_qip/device/optpulseprocessor.py | 4 +- src/qutip_qip/device/processor.py | 4 +- src/qutip_qip/operations/controlled.py | 20 +- src/qutip_qip/operations/gateclass.py | 9 +- .../operations/{std => gates}/__init__.py | 0 .../operations/{std => gates}/other_gates.py | 8 +- .../{std => gates}/single_qubit_gate.py | 34 +-- .../{std => gates}/two_qubit_gate.py | 60 ++-- src/qutip_qip/operations/old_gates.py | 2 +- src/qutip_qip/operations/utils.py | 9 +- src/qutip_qip/qasm.py | 54 ++-- src/qutip_qip/qiskit/utils/converter.py | 40 +-- src/qutip_qip/transpiler/chain.py | 2 +- .../test_single_qubit_gate_decompositions.py | 2 +- tests/test_bit_flip.py | 2 +- tests/test_circuit.py | 132 ++++----- tests/test_compiler.py | 2 +- tests/test_device.py | 2 +- tests/test_gates.py | 265 +++++++++--------- tests/test_noise.py | 2 +- tests/test_optpulseprocessor.py | 2 +- tests/test_phase_flip.py | 2 +- tests/test_processor.py | 2 +- tests/test_qasm.py | 48 ++-- tests/test_qiskit.py | 2 +- tests/test_renderer.py | 2 +- tests/test_scheduler.py | 2 +- tests/test_vqa.py | 2 +- 39 files changed, 397 insertions(+), 353 deletions(-) rename src/qutip_qip/operations/{std => gates}/__init__.py (100%) rename src/qutip_qip/operations/{std => gates}/other_gates.py (93%) rename src/qutip_qip/operations/{std => gates}/single_qubit_gate.py (93%) rename src/qutip_qip/operations/{std => gates}/two_qubit_gate.py (93%) diff --git a/src/qutip_qip/algorithms/bit_flip.py b/src/qutip_qip/algorithms/bit_flip.py index 18b3551cd..34494a944 100644 --- a/src/qutip_qip/algorithms/bit_flip.py +++ b/src/qutip_qip/algorithms/bit_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import CX, TOFFOLI, X +from qutip_qip.operations.gates import CX, TOFFOLI, X class BitFlipCode: diff --git a/src/qutip_qip/algorithms/phase_flip.py b/src/qutip_qip/algorithms/phase_flip.py index 6839f9a42..5992747f2 100644 --- a/src/qutip_qip/algorithms/phase_flip.py +++ b/src/qutip_qip/algorithms/phase_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import CX, H +from qutip_qip.operations.gates import CX, H class PhaseFlipCode: diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index 4d647d52c..c9e989923 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -7,7 +7,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.decompose import decompose_one_qubit_gate from qutip_qip.operations import expand_operator -from qutip_qip.operations.std import H, RZ, CX, CPHASE, SWAP +from qutip_qip.operations.gates import H, RZ, CX, CPHASE, SWAP def qft(N=1): diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 7a06e7bc3..377bfdb79 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -2,7 +2,7 @@ from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import unitary_gate, controlled, Gate -from qutip_qip.operations.std import H +from qutip_qip.operations.gates import H def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 330c0cdbb..9ecdc998f 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -5,7 +5,7 @@ """ import numpy as np -from qutip_qip.operations.std import ( +from qutip_qip.operations.gates import ( RX, RY, RZ, diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 5cdf5f1c6..2ad3b9980 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -16,7 +16,7 @@ ) from qutip_qip.circuit.utils import _check_iterable, _check_limit_ from qutip_qip.operations import Gate, Measurement, expand_operator -from qutip_qip.operations.std import RX, RY, RZ, GLOBALPHASE, GATE_CLASS_MAP +from qutip_qip.operations.gates import RX, RY, RZ, GLOBALPHASE, GATE_CLASS_MAP from qutip_qip.typing import IntList try: @@ -116,6 +116,7 @@ def gates(self) -> list[CircuitInstruction]: return self._instructions gates.setter + def gates(self) -> None: warnings.warn( "QubitCircuit.gates has been replaced with QubitCircuit.instructions", @@ -247,7 +248,7 @@ def add_measurement( def add_gate( self, gate: Gate | str, - targets: int | Iterable[int] =(), + targets: int | Iterable[int] = (), controls: int | Iterable[int] = (), arg_value: any = None, arg_label: str | None = None, @@ -362,10 +363,14 @@ def add_gate( gate = gate_class if gate.is_controlled() and len(controls) != gate.num_ctrl_qubits: - raise ValueError(f"{gate.name} takes {gate.num_ctrl_qubits} qubits, but {len(controls)} were provided.") + raise ValueError( + f"{gate.name} takes {gate.num_ctrl_qubits} qubits, but {len(controls)} were provided." + ) if len(controls) + len(targets) != gate.num_qubits: - raise ValueError(f"{gate.name} takes {gate.num_qubits} qubits, but {len(controls) + len(targets)} were provided.") + raise ValueError( + f"{gate.name} takes {gate.num_qubits} qubits, but {len(controls) + len(targets)} were provided." + ) qubits = [] if controls is not None: diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 78d35d100..457aa5a5a 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -2,7 +2,7 @@ from qutip_qip.circuit import GateInstruction from qutip_qip.compiler import GateCompiler, PulseInstruction -from qutip_qip.operations.std import RZ +from qutip_qip.operations.gates import RZ class CavityQEDCompiler(GateCompiler): diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index affc70934..30abc9b5c 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -2,7 +2,7 @@ from qutip_qip.circuit import GateInstruction from qutip_qip.compiler import GateCompiler, PulseInstruction -from qutip_qip.operations.std import RX, RY, RZX +from qutip_qip.operations.gates import RX, RY, RZX class SCQubitsCompiler(GateCompiler): @@ -69,7 +69,7 @@ class SCQubitsCompiler(GateCompiler): >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import ModelProcessor, SCQubitsModel >>> from qutip_qip.compiler import SCQubitsCompiler - >>> from qutip_qip.operations.std import CX + >>> from qutip_qip.operations.gates import CX >>> >>> qc = QubitCircuit(2) >>> qc.add_gate(CX, targets=0, controls=1) diff --git a/src/qutip_qip/compiler/scheduler.py b/src/qutip_qip/compiler/scheduler.py index 649c1f821..ad3ff562e 100644 --- a/src/qutip_qip/compiler/scheduler.py +++ b/src/qutip_qip/compiler/scheduler.py @@ -426,7 +426,7 @@ def schedule( -------- >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.compiler import Scheduler - >>> from qutip_qip.operations.std import H, CZ, SWAP + >>> from qutip_qip.operations.gates import H, CZ, SWAP >>> circuit = QubitCircuit(7) >>> circuit.add_gate(H, targets=3) # gate0 >>> circuit.add_gate(CZ, targets=5, controls=3) # gate1 diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index f1d9e9ebb..d132d5a74 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -3,7 +3,7 @@ import cmath from qutip_qip.decompose._utility import check_gate -from qutip_qip.operations.std import GLOBALPHASE, X, RX, RY, RZ +from qutip_qip.operations.gates import GLOBALPHASE, X, RX, RY, RZ class MethodError(Exception): diff --git a/src/qutip_qip/device/circuitqed.py b/src/qutip_qip/device/circuitqed.py index 01b27d786..45e932ec1 100644 --- a/src/qutip_qip/device/circuitqed.py +++ b/src/qutip_qip/device/circuitqed.py @@ -46,7 +46,7 @@ class SCQubits(ModelProcessor): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import SCQubits - from qutip_qip.operations.std import RY, RZ, CX + from qutip_qip.operations.gates import RY, RZ, CX qc = QubitCircuit(2) qc.add_gate(RZ, targets=0, arg_value=np.pi) diff --git a/src/qutip_qip/device/optpulseprocessor.py b/src/qutip_qip/device/optpulseprocessor.py index f005099bf..26a6e9ac8 100644 --- a/src/qutip_qip/device/optpulseprocessor.py +++ b/src/qutip_qip/device/optpulseprocessor.py @@ -70,7 +70,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations.std import H + >>> from qutip_qip.operations.gates import H >>> qc = QubitCircuit(1) >>> qc.add_gate(H, targets=0) >>> num_tslots = 10 @@ -85,7 +85,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations.std import H, SWAP, CX + >>> from qutip_qip.operations.gates import H, SWAP, CX >>> qc = QubitCircuit(2) >>> qc.add_gate(H, targets=0) >>> qc.add_gate(SWAP, targets=[0, 1]) diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 1d235326f..6e12dcc9b 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -4,7 +4,7 @@ import numpy as np from qutip import Qobj, QobjEvo, mesolve, mcsolve -from qutip_qip.operations.std import GLOBALPHASE +from qutip_qip.operations.gates import GLOBALPHASE from qutip_qip.noise import Noise, process_noise from qutip_qip.device import Model from qutip_qip.device.utils import _pulse_interpolate @@ -1120,7 +1120,7 @@ def run_state( init_state = states if analytical: if kwargs or self.noise: - raise warnings.warn( # FIXME this should raise an Error Type + raise warnings.warn( # FIXME this should raise an Error Type "Analytical matrices exponentiation" "does not process noise or" "any keyword arguments." diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 16041a227..26f86a81b 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -12,14 +12,19 @@ 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): if instance is None: - return partial(self.func, owner) # Called on the class (e.g., CX.get_qobj()) - - return partial(self.func, instance) # Called on the instance (e.g., CRX(0.5).get_qobj()) + return partial( + self.func, owner + ) # Called on the class (e.g., CX.get_qobj()) + + return partial( + self.func, instance + ) # Called on the instance (e.g., CRX(0.5).get_qobj()) class ControlledGate(Gate): @@ -47,7 +52,8 @@ class ControlledGate(Gate): * If the gate should execute when two control qubits are $|10\rangle$ (binary 10), set ``ctrl_value=0b10``. """ - __slots__ = ('_target_inst') + + __slots__ = "_target_inst" num_ctrl_qubits: int ctrl_value: int @@ -173,7 +179,7 @@ def get_qobj(cls_or_self) -> Qobj: num_controls=cls_or_self.num_ctrl_qubits, control_value=cls_or_self.ctrl_value, ) - + return controlled_gate_unitary( U=cls_or_self._target_inst.get_qobj(), num_controls=cls_or_self.num_ctrl_qubits, @@ -186,7 +192,7 @@ def inverse(cls_or_self) -> Gate: return controlled( cls_or_self.target_gate.inverse(), cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value + cls_or_self.ctrl_value, ) inverse_target_gate = cls_or_self._target_inst.inverse() @@ -194,7 +200,7 @@ def inverse(cls_or_self) -> Gate: inverse = controlled( type(inverse_target_gate), cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value + cls_or_self.ctrl_value, ) return inverse(arg_value=arg_value) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 923558e87..f1655d48b 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -56,7 +56,6 @@ def __init__(cls, name, bases, attrs): # don't overwrite any defaults like num_qubits etc in __setattr__. cls._is_frozen = True - def __setattr__(cls, name: str, value: any) -> None: """ One of the main purpose of this meta class is to enforce read-only constraints @@ -72,13 +71,15 @@ class X(Gate): 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 + # 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 cls._read_only_set: + if ( + cls.__dict__.get("_is_frozen", False) + and name in cls._read_only_set + ): raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) - def __str__(self) -> str: return f"Gate({self.name})" diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/gates/__init__.py similarity index 100% rename from src/qutip_qip/operations/std/__init__.py rename to src/qutip_qip/operations/gates/__init__.py diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py similarity index 93% rename from src/qutip_qip/operations/std/other_gates.py rename to src/qutip_qip/operations/gates/other_gates.py index 5c85fd644..6f78e2064 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -5,7 +5,7 @@ from qutip import Qobj from qutip_qip.operations import ControlledGate, AngleParametricGate -from qutip_qip.operations.std import X, SWAP +from qutip_qip.operations.gates import X, SWAP class GLOBALPHASE(AngleParametricGate): @@ -14,7 +14,7 @@ class GLOBALPHASE(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations.std import GLOBALPHASE + >>> from qutip_qip.operations.gates import GLOBALPHASE """ num_qubits: int = 0 @@ -47,7 +47,7 @@ class TOFFOLI(ControlledGate): Examples -------- - >>> from qutip_qip.operations.std import TOFFOLI + >>> from qutip_qip.operations.gates import TOFFOLI >>> TOFFOLI.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = @@ -93,7 +93,7 @@ class FREDKIN(ControlledGate): Examples -------- - >>> from qutip_qip.operations.std import FREDKIN + >>> from qutip_qip.operations.gates import FREDKIN >>> FREDKIN.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py similarity index 93% rename from src/qutip_qip/operations/std/single_qubit_gate.py rename to src/qutip_qip/operations/gates/single_qubit_gate.py index eeb1a5755..373c5d107 100644 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -26,7 +26,7 @@ class X(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import X + >>> from qutip_qip.operations.gates import X >>> X.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -52,7 +52,7 @@ class Y(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import Y + >>> from qutip_qip.operations.gates import Y >>> Y.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -78,7 +78,7 @@ class Z(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import Z + >>> from qutip_qip.operations.gates import Z >>> Z.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -104,7 +104,7 @@ class IDLE(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import IDLE + >>> from qutip_qip.operations.gates import IDLE """ __slots__ = () @@ -125,7 +125,7 @@ class H(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import H + >>> from qutip_qip.operations.gates import H >>> H.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True Qobj data = @@ -168,7 +168,7 @@ class SQRTX(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTX + >>> from qutip_qip.operations.gates import SQRTX >>> SQRTX.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -198,7 +198,7 @@ class SQRTXdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTXdag + >>> from qutip_qip.operations.gates import SQRTXdag >>> SQRTXdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -241,7 +241,7 @@ class S(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import S + >>> from qutip_qip.operations.gates import S >>> S.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -271,7 +271,7 @@ class Sdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import S + >>> from qutip_qip.operations.gates import S >>> Sdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -301,7 +301,7 @@ class T(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import T + >>> from qutip_qip.operations.gates import T >>> T.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -330,7 +330,7 @@ class Tdag(_SingleQubitGate): Examples -------- - >>> from qutip_qip.operations.std import Tdag + >>> from qutip_qip.operations.gates import Tdag >>> Tdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -359,7 +359,7 @@ class RX(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import RX + >>> from qutip_qip.operations.gates import RX >>> RX(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -393,7 +393,7 @@ class RY(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import RY + >>> from qutip_qip.operations.gates import RY >>> RY(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -426,7 +426,7 @@ class RZ(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import RZ + >>> from qutip_qip.operations.gates import RZ >>> RZ(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -454,7 +454,7 @@ class PHASE(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import PHASE + >>> from qutip_qip.operations.gates import PHASE """ __slots__ = () @@ -489,7 +489,7 @@ class R(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import R + >>> from qutip_qip.operations.gates import R >>> R((np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = @@ -531,7 +531,7 @@ class QASMU(_SingleQubitParametricGate): Examples -------- - >>> from qutip_qip.operations.std import QASMU + >>> from qutip_qip.operations.gates import QASMU >>> QASMU(0, (np.pi/2, np.pi, np.pi/2)).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py similarity index 93% rename from src/qutip_qip/operations/std/two_qubit_gate.py rename to src/qutip_qip/operations/gates/two_qubit_gate.py index 9e0abe4be..2ec79106e 100644 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -6,7 +6,19 @@ from qutip import Qobj from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate -from qutip_qip.operations.std import X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE +from qutip_qip.operations.gates import ( + X, + Y, + Z, + H, + S, + T, + RX, + RY, + RZ, + QASMU, + PHASE, +) class _TwoQubitGate(Gate): @@ -36,7 +48,7 @@ class SWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SWAP + >>> from qutip_qip.operations.gates import SWAP >>> SWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -66,7 +78,7 @@ class ISWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import ISWAP + >>> from qutip_qip.operations.gates import ISWAP >>> ISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -100,7 +112,7 @@ class ISWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import ISWAPdag + >>> from qutip_qip.operations.gates import ISWAPdag >>> ISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -134,7 +146,7 @@ class SQRTSWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTSWAP + >>> from qutip_qip.operations.gates import SQRTSWAP >>> SQRTSWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -174,7 +186,7 @@ class SQRTSWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTSWAPdag + >>> from qutip_qip.operations.gates import SQRTSWAPdag >>> SQRTSWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -214,7 +226,7 @@ class SQRTISWAP(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTISWAP + >>> from qutip_qip.operations.gates import SQRTISWAP >>> SQRTISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -255,7 +267,7 @@ class SQRTISWAPdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import SQRTISWAPdag + >>> from qutip_qip.operations.gates import SQRTISWAPdag >>> SQRTISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -305,7 +317,7 @@ class BERKELEY(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import BERKELEY + >>> from qutip_qip.operations.gates import BERKELEY >>> BERKELEY.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -352,7 +364,7 @@ class BERKELEYdag(_TwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import BERKELEYdag + >>> from qutip_qip.operations.gates import BERKELEYdag >>> BERKELEYdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -399,7 +411,7 @@ class SWAPALPHA(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations.std import SWAPALPHA + >>> from qutip_qip.operations.gates import SWAPALPHA >>> SWAPALPHA(0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -457,7 +469,7 @@ class MS(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations.std import MS + >>> from qutip_qip.operations.gates import MS >>> MS((np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -515,7 +527,7 @@ class RZX(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations.std import RZX + >>> from qutip_qip.operations.gates import RZX >>> RZX(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -556,7 +568,7 @@ class CX(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CX + >>> from qutip_qip.operations.gates import CX >>> CX.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -600,7 +612,7 @@ class CY(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CY + >>> from qutip_qip.operations.gates import CY >>> CY.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -631,7 +643,7 @@ class CZ(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CZ + >>> from qutip_qip.operations.gates import CZ >>> CZ.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True Qobj data = @@ -684,7 +696,7 @@ class CH(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CH + >>> from qutip_qip.operations.gates import CH """ __slots__ = () @@ -722,7 +734,7 @@ class CT(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CT + >>> from qutip_qip.operations.gates import CT """ __slots__ = () @@ -759,7 +771,7 @@ class CS(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CS + >>> from qutip_qip.operations.gates import CS """ __slots__ = () @@ -793,7 +805,7 @@ class CPHASE(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CPHASE + >>> from qutip_qip.operations.gates import CPHASE >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -816,7 +828,7 @@ class CRX(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CRX + >>> from qutip_qip.operations.gates import CRX """ __slots__ = () @@ -832,7 +844,7 @@ class CRY(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CRY + >>> from qutip_qip.operations.gates import CRY """ __slots__ = () @@ -857,7 +869,7 @@ class CRZ(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CRZ + >>> from qutip_qip.operations.gates import CRZ >>> CRZ(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False Qobj data = @@ -880,7 +892,7 @@ class CQASMU(_ControlledTwoQubitGate): Examples -------- - >>> from qutip_qip.operations.std import CQASMU + >>> from qutip_qip.operations.gates import CQASMU """ __slots__ = () diff --git a/src/qutip_qip/operations/old_gates.py b/src/qutip_qip/operations/old_gates.py index f84d918cf..dcc314364 100644 --- a/src/qutip_qip/operations/old_gates.py +++ b/src/qutip_qip/operations/old_gates.py @@ -1113,7 +1113,7 @@ def qubit_clifford_group(N=None, target=0): warnings.warn( "qubit_clifford has been deprecated and will be removed in future version.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) # The Ross-Selinger presentation of the single-qubit Clifford diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index bf308f364..4e65adaf6 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -45,7 +45,7 @@ def _check_oper_dims( def _targets_to_list( targets: int | Iterable[int], oper: Qobj | None = None, - N: int | None = None + N: int | None = None, ) -> list[int]: """ transform targets to a list and check validity. @@ -85,6 +85,7 @@ def _targets_to_list( raise ValueError("Targets must be smaller than N={}.".format(N)) return targets + def expand_operator( oper: Qobj, dims: Iterable[int], @@ -119,7 +120,7 @@ def expand_operator( -------- >>> import qutip >>> from qutip_qip.operations import expand_operator - >>> from qutip_qip.operations.std import X, CX + >>> from qutip_qip.operations.gates import X, CX >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True Qobj data = @@ -181,6 +182,7 @@ def expand_operator( out = tensor([oper] + id_list).permute(new_order) return out.to(dtype) + def hadamard_transform(N=1): """Quantum object representing the N-qubit Hadamard gate. @@ -195,11 +197,12 @@ def hadamard_transform(N=1): return tensor([H] * N) + def gate_sequence_product( U_list: list[Qobj], left_to_right: bool = True, inds_list: list[list[int]] | None = None, - expand: bool = False + expand: bool = False, ) -> Qobj | tuple[Qobj, list[int]]: """ Calculate the overall unitary matrix for a given list of unitary operations. diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index e25918563..60ac98991 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -11,7 +11,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import unitary_gate -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates __all__ = ["read_qasm", "save_qasm", "print_qasm", "circuit_to_qasm_str"] @@ -537,14 +537,14 @@ def _add_qiskit_gates( """ gate_name_map_1q = { - "x": std.X, - "y": std.Y, - "z": std.Z, - "h": std.H, - "t": std.T, - "s": std.S, - "sdg": std.Sdag, - "tdg": std.Tdag, + "x": gates.X, + "y": gates.Y, + "z": gates.Z, + "h": gates.H, + "t": gates.T, + "s": gates.S, + "sdg": gates.Sdag, + "tdg": gates.Tdag, } if len(args) == 0: args = None @@ -553,7 +553,7 @@ def _add_qiskit_gates( if name == "u3": qc.add_gate( - std.QASMU(args), + gates.QASMU(args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -561,28 +561,28 @@ def _add_qiskit_gates( elif name == "u2": u2_args = [np.pi / 2, args[0], args[1]] qc.add_gate( - std.QASMU(u2_args), + gates.QASMU(u2_args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "id": qc.add_gate( - std.IDLE, + gates.IDLE, targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "u1": qc.add_gate( - std.RZ(args), + gates.RZ(args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "cz": qc.add_gate( - std.CZ, + gates.CZ, targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -590,7 +590,7 @@ def _add_qiskit_gates( ) elif name == "cy": qc.add_gate( - std.CY, + gates.CY, targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -598,7 +598,7 @@ def _add_qiskit_gates( ) elif name == "ch": qc.add_gate( - std.CH, + gates.CH, targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -606,14 +606,14 @@ def _add_qiskit_gates( ) elif name == "swap": qc.add_gate( - std.SWAP, + gates.SWAP, controls=regs, classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "ccx": qc.add_gate( - std.TOFFOLI, + gates.TOFFOLI, targets=regs[2], controls=regs[:2], classical_controls=classical_controls, @@ -621,7 +621,7 @@ def _add_qiskit_gates( ) elif name == "crz": qc.add_gate( - std.CRZ(arg_value=args), + gates.CRZ(arg_value=args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -629,7 +629,7 @@ def _add_qiskit_gates( ) elif name == "cu1": qc.add_gate( - std.CPHASE(arg_value=args), + gates.CPHASE(arg_value=args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -637,7 +637,7 @@ def _add_qiskit_gates( ) elif name == "cu3": qc.add_gate( - std.CQASMU(arg_value=args), + gates.CQASMU(arg_value=args), controls=regs[0], targets=[regs[1]], classical_controls=classical_controls, @@ -645,7 +645,7 @@ def _add_qiskit_gates( ) elif name == "cx": qc.add_gate( - std.CX, + gates.CX, targets=int(regs[1]), controls=int(regs[0]), classical_controls=classical_controls, @@ -653,21 +653,21 @@ def _add_qiskit_gates( ) elif name == "rx": qc.add_gate( - std.RX(args), + gates.RX(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "ry": qc.add_gate( - std.RY(args), + gates.RY(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "rz": qc.add_gate( - std.RZ(args), + gates.RZ(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -715,7 +715,7 @@ def _add_predefined_gates( if name == "CX": qc.add_gate( - std.CX, + gates.CX, targets=int(com_regs[1]), controls=int(com_regs[0]), classical_controls=classical_controls, @@ -723,7 +723,7 @@ def _add_predefined_gates( ) elif name == "U": qc.add_gate( - std.QASMU(arg_value=[float(arg) for arg in com_args]), + gates.QASMU(arg_value=[float(arg) for arg in com_args]), targets=int(com_regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index 444d99409..96fd99655 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -3,32 +3,32 @@ from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import Gate -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates # TODO Expand this dictionary for other gates like CS etc. _map_gates: dict[str, Gate] = { - "p": std.PHASE, - "x": std.X, - "y": std.Y, - "z": std.Z, - "h": std.H, - "s": std.S, - "t": std.T, - "rx": std.RX, - "ry": std.RY, - "rz": std.RZ, - "swap": std.SWAP, - "u": std.QASMU, + "p": gates.PHASE, + "x": gates.X, + "y": gates.Y, + "z": gates.Z, + "h": gates.H, + "s": gates.S, + "t": gates.T, + "rx": gates.RX, + "ry": gates.RY, + "rz": gates.RZ, + "swap": gates.SWAP, + "u": gates.QASMU, } _map_controlled_gates: dict[str, Gate] = { - "cx": std.CX, - "cy": std.CY, - "cz": std.CZ, - "crx": std.CRX, - "cry": std.CRY, - "crz": std.CRZ, - "cp": std.CPHASE, + "cx": gates.CX, + "cy": gates.CY, + "cz": gates.CZ, + "crx": gates.CRX, + "cry": gates.CRY, + "crz": gates.CRZ, + "cp": gates.CPHASE, } _ignore_gates: list[str] = ["id", "barrier"] diff --git a/src/qutip_qip/transpiler/chain.py b/src/qutip_qip/transpiler/chain.py index ecae982ee..a9e50807c 100644 --- a/src/qutip_qip/transpiler/chain.py +++ b/src/qutip_qip/transpiler/chain.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import SWAP +from qutip_qip.operations.gates import SWAP def to_chain_structure(qc: QubitCircuit, setup="linear"): diff --git a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py index f5733ec66..ab1c75683 100644 --- a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py +++ b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py @@ -9,7 +9,7 @@ ) from qutip_qip.decompose import decompose_one_qubit_gate from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import H, X, Y, Z, S, T, SQRTX +from qutip_qip.operations.gates import H, X, Y, Z, S, T, SQRTX # Fidelity closer to 1 means the two states are similar to each other target = 0 diff --git a/tests/test_bit_flip.py b/tests/test_bit_flip.py index e8ca5ce74..7089f789f 100644 --- a/tests/test_bit_flip.py +++ b/tests/test_bit_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import BitFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import X +from qutip_qip.operations.gates import X @pytest.fixture diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 0b6dd5b2c..4ba8ea0c8 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -20,7 +20,7 @@ ) from qutip_qip.qasm import read_qasm from qutip_qip.operations import Gate, Measurement, gate_sequence_product -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates from qutip_qip.transpiler import to_chain_structure from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation @@ -36,14 +36,14 @@ def _teleportation_circuit(): 3, num_cbits=2, input_states=["q0", "0", "0", "c0", "c1"] ) - teleportation.add_gate(std.H, targets=[1]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CX, targets=[1], controls=[0]) - teleportation.add_gate(std.H, targets=[0]) + teleportation.add_gate(gates.H, targets=[1]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CX, targets=[1], controls=[0]) + teleportation.add_gate(gates.H, targets=[0]) teleportation.add_measurement("M0", targets=[0], classical_store=1) teleportation.add_measurement("M1", targets=[1], classical_store=0) - teleportation.add_gate(std.X, targets=[2], classical_controls=[0]) - teleportation.add_gate(std.Z, targets=[2], classical_controls=[1]) + teleportation.add_gate(gates.X, targets=[2], classical_controls=[0]) + teleportation.add_gate(gates.Z, targets=[2], classical_controls=[1]) return teleportation @@ -53,12 +53,12 @@ def _teleportation_circuit2(): 3, num_cbits=2, input_states=["q0", "0", "0", "c0", "c1"] ) - teleportation.add_gate(std.H, targets=[1]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CX, targets=[1], controls=[0]) - teleportation.add_gate(std.H, targets=[0]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CZ, targets=[2], controls=[0]) + teleportation.add_gate(gates.H, targets=[1]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CX, targets=[1], controls=[0]) + teleportation.add_gate(gates.H, targets=[0]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CZ, targets=[2], controls=[0]) return teleportation @@ -80,13 +80,13 @@ class TestQubitCircuit: @pytest.mark.parametrize( ["gate_from", "gate_to", "targets", "controls"], [ - pytest.param(std.SWAP, "CX", [0, 1], [], id="SWAPtoCX"), - pytest.param(std.ISWAP, "CX", [0, 1], [], id="ISWAPtoCX"), - pytest.param(std.CZ, "CX", [1], [0], id="CZtoCX"), - pytest.param(std.CX, "CZ", [0], [1], id="CXtoCZ"), - pytest.param(std.CX, "SQRTSWAP", [0], [1], id="CXtoSQRTSWAP"), - pytest.param(std.CX, "SQRTISWAP", [0], [1], id="CXtoSQRTISWAP"), - pytest.param(std.CX, "ISWAP", [0], [1], id="CXtoISWAP"), + pytest.param(gates.SWAP, "CX", [0, 1], [], id="SWAPtoCX"), + pytest.param(gates.ISWAP, "CX", [0, 1], [], id="ISWAPtoCX"), + pytest.param(gates.CZ, "CX", [1], [0], id="CZtoCX"), + pytest.param(gates.CX, "CZ", [0], [1], id="CXtoCZ"), + pytest.param(gates.CX, "SQRTSWAP", [0], [1], id="CXtoSQRTSWAP"), + pytest.param(gates.CX, "SQRTISWAP", [0], [1], id="CXtoSQRTISWAP"), + pytest.param(gates.CX, "ISWAP", [0], [1], id="CXtoISWAP"), ], ) def testresolve(self, gate_from, gate_to, targets, controls): @@ -103,7 +103,7 @@ def testHdecompose(self): resolved matrices in terms of rotation gates. """ qc1 = QubitCircuit(1) - qc1.add_gate(std.H, targets=0) + qc1.add_gate(gates.H, targets=0) U1 = qc1.compute_unitary() qc2 = qc1.resolve_gates() U2 = qc2.compute_unitary() @@ -115,7 +115,7 @@ def testFREDKINdecompose(self): resolved matrices in terms of rotation gates and CNOT. """ qc1 = QubitCircuit(3) - qc1.add_gate(std.FREDKIN, targets=[0, 1], controls=[2]) + qc1.add_gate(gates.FREDKIN, targets=[0, 1], controls=[2]) U1 = qc1.compute_unitary() qc2 = qc1.resolve_gates() U2 = qc2.compute_unitary() @@ -126,14 +126,14 @@ def test_add_gate(self): Addition of a gate object directly to a `QubitCircuit` """ qc = QubitCircuit(6) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(std.H, targets=[3]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.RY(arg_value=1.570796), targets=4) - qc.add_gate(std.RY(arg_value=1.570796), targets=5) - qc.add_gate(std.RX(arg_value=-1.570796), targets=[3]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.H, targets=[3]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.RY(arg_value=1.570796), targets=4) + qc.add_gate(gates.RY(arg_value=1.570796), targets=5) + qc.add_gate(gates.RX(arg_value=-1.570796), targets=[3]) # Test explicit gate addition assert qc.instructions[0].operation.name == "CX" @@ -201,15 +201,15 @@ def test_add_circuit(self): """ qc = QubitCircuit(6) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(std.H, targets=[3]) - qc.add_gate(std.SWAP, targets=[1, 4]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.H, targets=[3]) + qc.add_gate(gates.SWAP, targets=[1, 4]) qc.add_measurement("M0", targets=[0], classical_store=[1]) - qc.add_gate(std.RY(1.570796), targets=4) - qc.add_gate(std.RY(1.570796), targets=5) - qc.add_gate(std.CRX(arg_value=np.pi / 2), controls=[1], targets=[2]) + qc.add_gate(gates.RY(1.570796), targets=4) + qc.add_gate(gates.RY(1.570796), targets=5) + qc.add_gate(gates.CRX(arg_value=np.pi / 2), controls=[1], targets=[2]) qc1 = QubitCircuit(6) qc1.add_circuit(qc) @@ -309,10 +309,10 @@ def test_add_measurement(self): qc = QubitCircuit(3, num_cbits=3) qc.add_measurement("M0", targets=[0], classical_store=0) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) qc.add_measurement("M1", targets=[2], classical_store=1) - qc.add_gate(std.H, targets=[1], classical_controls=[0, 1]) + qc.add_gate(gates.H, targets=[1], classical_controls=[0, 1]) qc.add_measurement("M2", targets=[1], classical_store=2) # checking correct addition of measurements @@ -325,7 +325,9 @@ def test_add_measurement(self): assert qc.instructions[2].operation.name == "TOFFOLI" assert qc.instructions[4].cbits == (0, 1) - @pytest.mark.parametrize("gate", [std.X, std.Y, std.Z, std.S, std.T]) + @pytest.mark.parametrize( + "gate", [gates.X, gates.Y, gates.Z, gates.S, gates.T] + ) def test_exceptions(self, gate): """ Text exceptions are thrown correctly for inadequate inputs @@ -339,15 +341,15 @@ def test_single_qubit_gates(self): """ qc = QubitCircuit(3) - qc.add_gate(std.X, targets=[0]) - qc.add_gate(std.CY, targets=[1], controls=[0]) - qc.add_gate(std.Y, targets=[2]) - qc.add_gate(std.CS, targets=[0], controls=[1]) - qc.add_gate(std.Z, targets=[1]) - qc.add_gate(std.CT, targets=[1], controls=[2]) - qc.add_gate(std.CZ, targets=[0], controls=[1]) - qc.add_gate(std.S, targets=[1]) - qc.add_gate(std.T, targets=[2]) + qc.add_gate(gates.X, targets=[0]) + qc.add_gate(gates.CY, targets=[1], controls=[0]) + qc.add_gate(gates.Y, targets=[2]) + qc.add_gate(gates.CS, targets=[0], controls=[1]) + qc.add_gate(gates.Z, targets=[1]) + qc.add_gate(gates.CT, targets=[1], controls=[2]) + qc.add_gate(gates.CZ, targets=[0], controls=[1]) + qc.add_gate(gates.S, targets=[1]) + qc.add_gate(gates.T, targets=[2]) assert qc.instructions[8].operation.name == "T" assert qc.instructions[7].operation.name == "S" @@ -380,10 +382,10 @@ def test_reverse(self): """ qc = QubitCircuit(3, num_cbits=1) - qc.add_gate(std.RX(arg_value=3.141, arg_label=r"\pi/2"), targets=[0]) - qc.add_gate(std.CX, targets=[1], controls=[0]) + qc.add_gate(gates.RX(arg_value=3.141, arg_label=r"\pi/2"), targets=[0]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) qc.add_measurement("M1", targets=[1], classical_store=0) - qc.add_gate(std.H, targets=[2]) + qc.add_gate(gates.H, targets=[2]) # Keep input output same qc.add_state("0", targets=[0]) @@ -409,7 +411,7 @@ def test_user_gate(self): def customer_gate1(arg_values): mat = np.zeros((4, 4), dtype=np.complex128) mat[0, 0] = mat[1, 1] = 1.0 - mat[2:4, 2:4] = std.RX(arg_values).get_qobj().full() + mat[2:4, 2:4] = gates.RX(arg_values).get_qobj().full() return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): @@ -425,7 +427,7 @@ def get_qobj(): return Qobj(mat, dims=[[2], [2]]) qc = QubitCircuit(3) - qc.add_gate(std.CRX(arg_value=np.pi / 2), targets=[2], controls=[1]) + qc.add_gate(gates.CRX(arg_value=np.pi / 2), targets=[2], controls=[1]) qc.add_gate(T1, targets=[1]) props = qc.propagators() result1 = tensor(identity(2), customer_gate1(np.pi / 2)) @@ -496,7 +498,7 @@ def test_run_teleportation(self): def test_classical_control(self): qc = QubitCircuit(1, num_cbits=2) qc.add_gate( - std.X, + gates.X, targets=[0], classical_controls=[0, 1], classical_control_value=1, @@ -507,7 +509,7 @@ def test_classical_control(self): qc = QubitCircuit(1, num_cbits=2) qc.add_gate( - std.X, + gates.X, targets=[0], classical_controls=[0, 1], classical_control_value=2, @@ -572,7 +574,7 @@ def test_measurement_circuit(self): def test_circuit_with_selected_measurement_result(self): qc = QubitCircuit(num_qubits=1, num_cbits=1) - qc.add_gate(std.H, targets=0) + qc.add_gate(gates.H, targets=0) qc.add_measurement("M0", targets=0, classical_store=0) # We reset the random seed so that @@ -668,7 +670,7 @@ def test_latex_code_teleportation_circuit(self): def test_latex_code_classical_controls(self): qc = QubitCircuit(1, num_cbits=1, reverse_states=True) - qc.add_gate(std.X, targets=0, classical_controls=[0]) + qc.add_gate(gates.X, targets=0, classical_controls=[0]) renderer = TeXRenderer(qc) latex = TeXRenderer(qc).latex_code() assert latex == renderer._latex_template % "\n".join( @@ -680,7 +682,7 @@ def test_latex_code_classical_controls(self): ) qc = QubitCircuit(1, num_cbits=1, reverse_states=False) - qc.add_gate(std.X, targets=0, classical_controls=[0]) + qc.add_gate(gates.X, targets=0, classical_controls=[0]) renderer = TeXRenderer(qc) latex = TeXRenderer(qc).latex_code() assert latex == renderer._latex_template % "\n".join( @@ -698,7 +700,7 @@ def test_latex_code_classical_controls(self): H_zyz_quantum_circuit = QubitCircuit(1) for g in H_zyz_gates: H_zyz_quantum_circuit.add_gate(g, targets=[0]) # TODO CHECK - sigmax_zyz_gates = _ZYZ_rotation(std.X.get_qobj()) + sigmax_zyz_gates = _ZYZ_rotation(gates.X.get_qobj()) sigmax_zyz_quantum_circuit = QubitCircuit(1) for g in sigmax_zyz_gates: sigmax_zyz_quantum_circuit.add_gate(g, targets=[0]) @@ -707,7 +709,7 @@ def test_latex_code_classical_controls(self): "valid_input, correct_result", [ (H_zyz_quantum_circuit, H), - (sigmax_zyz_quantum_circuit, std.X.get_qobj()), + (sigmax_zyz_quantum_circuit, gates.X.get_qobj()), ], ) def test_compute_unitary(self, valid_input, correct_result): @@ -742,7 +744,7 @@ def test_export_image(self, in_temporary_directory): from qutip_qip.circuit.draw import CONVERTERS qc = QubitCircuit(2, reverse_states=False) - qc.add_gate(std.CZ, controls=[0], targets=[1]) + qc.add_gate(gates.CZ, controls=[0], targets=[1]) if "png" in CONVERTERS: file_png200 = "exported_pic_200.png" @@ -762,7 +764,7 @@ def test_circuit_chain_structure(self): Test if the transpiler correctly inherit the properties of a circuit. """ qc = QubitCircuit(3, reverse_states=True) - qc.add_gate(std.CX, targets=[2], controls=[0]) + qc.add_gate(gates.CX, targets=[2], controls=[0]) qc2 = to_chain_structure(qc) assert qc2.reverse_states is True diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 7d8cb17d5..2437cde13 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -16,7 +16,7 @@ ) from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ParametricGate -from qutip_qip.operations.std import X, RX +from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity diff --git a/tests/test_device.py b/tests/test_device.py index 607f54f2b..01b43d9eb 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -10,7 +10,7 @@ import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import gate_sequence_product -from qutip_qip.operations.std import ( +from qutip_qip.operations.gates import ( X, Y, Z, diff --git a/tests/test_gates.py b/tests/test_gates.py index 749aa2c92..eab8b9cbd 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -5,9 +5,14 @@ import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import ( - Gate, expand_operator, controlled, unitary_gate, qubit_clifford_group, hadamard_transform + Gate, + expand_operator, + controlled, + unitary_gate, + qubit_clifford_group, + hadamard_transform, ) -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates def _permutation_id(permutation): @@ -80,7 +85,7 @@ def test_swap(self): states = [qutip.rand_ket(2) for _ in [None] * 2] start = qutip.tensor(states) swapped = qutip.tensor(states[::-1]) - swap = std.SWAP.get_qobj() + swap = gates.SWAP.get_qobj() assert _infidelity(swapped, swap * start) < 1e-12 assert _infidelity(start, swap * swap * start) < 1e-12 @@ -91,7 +96,7 @@ def test_swap(self): ) def test_toffoli(self, permutation): test = expand_operator( - std.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation + gates.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation ) base = qutip.tensor( 1 - qutip.basis([2, 2], [1, 1]).proj(), qutip.qeye(2) @@ -116,17 +121,17 @@ def test_toffoli(self, permutation): ) def test_molmer_sorensen(self, angle, expected): np.testing.assert_allclose( - std.MS(angle).get_qobj().full(), expected.full(), atol=1e-15 + gates.MS(angle).get_qobj().full(), expected.full(), atol=1e-15 ) @pytest.mark.parametrize( ["gate", "n_angles"], [ - pytest.param(std.RX, 1, id="Rx"), - pytest.param(std.RY, 1, id="Ry"), - pytest.param(std.RZ, 1, id="Rz"), - pytest.param(std.PHASE, 1, id="phase"), - pytest.param(std.R, 2, id="Rabi rotation"), + pytest.param(gates.RX, 1, id="Rx"), + pytest.param(gates.RY, 1, id="Ry"), + pytest.param(gates.RZ, 1, id="Rz"), + pytest.param(gates.PHASE, 1, id="phase"), + pytest.param(gates.R, 2, id="Rabi rotation"), ], ) def test_zero_rotations_are_identity(self, gate, n_angles): @@ -188,16 +193,16 @@ class TestGateExpansion: @pytest.mark.parametrize( ["gate", "n_angles"], [ - pytest.param(std.RX, 1, id="Rx"), - pytest.param(std.RY, 1, id="Ry"), - pytest.param(std.RZ, 1, id="Rz"), - pytest.param(std.X, 0, id="X"), - pytest.param(std.Y, 0, id="Y"), - pytest.param(std.Z, 0, id="Z"), - pytest.param(std.S, 0, id="S"), - pytest.param(std.T, 0, id="T"), - pytest.param(std.PHASE, 1, id="phase"), - pytest.param(std.R, 2, id="Rabi rotation"), + pytest.param(gates.RX, 1, id="Rx"), + pytest.param(gates.RY, 1, id="Ry"), + pytest.param(gates.RZ, 1, id="Rz"), + pytest.param(gates.X, 0, id="X"), + pytest.param(gates.Y, 0, id="Y"), + pytest.param(gates.Z, 0, id="Z"), + pytest.param(gates.S, 0, id="S"), + pytest.param(gates.T, 0, id="T"), + pytest.param(gates.PHASE, 1, id="phase"), + pytest.param(gates.R, 2, id="Rabi rotation"), ], ) def test_single_qubit_rotation(self, gate: Gate, n_angles: int): @@ -211,7 +216,10 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): for target in range(self.n_qubits): start = qutip.tensor(random[:target] + [base] + random[target:]) test = ( - expand_operator(gate.get_qobj(), targets=target, dims=[2]*self.n_qubits) * start + expand_operator( + gate.get_qobj(), targets=target, dims=[2] * self.n_qubits + ) + * start ) expected = qutip.tensor( random[:target] + [applied] + random[target:] @@ -221,15 +229,17 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.CX, 1, id="CX"), - pytest.param(std.CY, 1, id="CY"), - pytest.param(std.CZ, 1, id="CZ"), - pytest.param(std.CS, 1, id="CS"), - pytest.param(std.CT, 1, id="CT"), - pytest.param(std.SWAP, 0, id="SWAP"), - pytest.param(std.ISWAP, 0, id="ISWAP"), - pytest.param(std.SQRTSWAP, 0, id="SQRTSWAP"), - pytest.param(std.MS([0.5 * np.pi, 0.0]), 0, id="Molmer-Sorensen"), + pytest.param(gates.CX, 1, id="CX"), + pytest.param(gates.CY, 1, id="CY"), + pytest.param(gates.CZ, 1, id="CZ"), + pytest.param(gates.CS, 1, id="CS"), + pytest.param(gates.CT, 1, id="CT"), + pytest.param(gates.SWAP, 0, id="SWAP"), + pytest.param(gates.ISWAP, 0, id="ISWAP"), + pytest.param(gates.SQRTSWAP, 0, id="SQRTSWAP"), + pytest.param( + gates.MS([0.5 * np.pi, 0.0]), 0, id="Molmer-Sorensen" + ), ], ) def test_two_qubit(self, gate, n_controls): @@ -248,11 +258,12 @@ def test_two_qubit(self, gate, n_controls): random_gate = unitary_gate("random", qutip.rand_unitary([2] * 1)) RandomThreeQubitGate = controlled(random_gate, 2) + @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.FREDKIN, 1, id="Fredkin"), - pytest.param(std.TOFFOLI, 2, id="Toffoli"), + pytest.param(gates.FREDKIN, 1, id="Fredkin"), + pytest.param(gates.TOFFOLI, 2, id="Toffoli"), pytest.param(RandomThreeQubitGate, 2, id="random"), ], ) @@ -265,7 +276,7 @@ def test_three_qubit(self, gate: Gate, n_controls): qubits = others.copy() qubits[q1], qubits[q2], qubits[q3] = targets test = expand_operator( - gate.get_qobj(), targets=[q1, q2, q3], dims=[2]*self.n_qubits + gate.get_qobj(), targets=[q1, q2, q3], dims=[2] * self.n_qubits ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement( qubits, reference, [q1, q2, q3] @@ -315,7 +326,7 @@ def test_general_qubit_expansion(self, n_targets): def test_cnot_explicit(self): test = expand_operator( - std.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] + gates.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] ).full() expected = np.array( [ @@ -375,11 +386,11 @@ def test_non_qubit_systems(self, dimensions): def test_dtype(self): expanded_qobj = expand_operator( - std.CX.get_qobj(), dims=[2, 2, 2] + gates.CX.get_qobj(), dims=[2, 2, 2] ).data assert isinstance(expanded_qobj, qutip.data.CSR) expanded_qobj = expand_operator( - std.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" + gates.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" ).data assert isinstance(expanded_qobj, qutip.data.Dense) @@ -388,115 +399,119 @@ def test_gates_class(): init_state = qutip.rand_ket([2, 2, 2]) circuit1 = QubitCircuit(3) - circuit1.add_gate(std.X, targets=1) - circuit1.add_gate(std.Y, targets=1) - circuit1.add_gate(std.Z, targets=2) - circuit1.add_gate(std.RX(np.pi / 4), targets=0) - circuit1.add_gate(std.RY(np.pi / 4), targets=0) - circuit1.add_gate(std.RZ(np.pi / 4), targets=1) - circuit1.add_gate(std.H, targets=0) - circuit1.add_gate(std.SQRTX, targets=0) - circuit1.add_gate(std.S, targets=2) - circuit1.add_gate(std.T, targets=1) - circuit1.add_gate(std.R(arg_value=(np.pi / 4, np.pi / 6)), targets=1) + circuit1.add_gate(gates.X, targets=1) + circuit1.add_gate(gates.Y, targets=1) + circuit1.add_gate(gates.Z, targets=2) + circuit1.add_gate(gates.RX(np.pi / 4), targets=0) + circuit1.add_gate(gates.RY(np.pi / 4), targets=0) + circuit1.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit1.add_gate(gates.H, targets=0) + circuit1.add_gate(gates.SQRTX, targets=0) + circuit1.add_gate(gates.S, targets=2) + circuit1.add_gate(gates.T, targets=1) + circuit1.add_gate(gates.R(arg_value=(np.pi / 4, np.pi / 6)), targets=1) circuit1.add_gate( - std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0 + gates.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0 ) - circuit1.add_gate(std.CX, controls=0, targets=1) - circuit1.add_gate(std.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) - circuit1.add_gate(std.SWAP, targets=[0, 1]) - circuit1.add_gate(std.ISWAP, targets=[2, 1]) - circuit1.add_gate(std.CZ, controls=[0], targets=[2]) - circuit1.add_gate(std.SQRTSWAP, [2, 0]) - circuit1.add_gate(std.SQRTISWAP, [0, 1]) - circuit1.add_gate(std.SWAPALPHA(np.pi / 4), [1, 2]) - circuit1.add_gate(std.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0]) - circuit1.add_gate(std.TOFFOLI, controls=[2, 0], targets=[1]) - circuit1.add_gate(std.FREDKIN, controls=[0], targets=[1, 2]) - circuit1.add_gate(std.BERKELEY, targets=[1, 0]) - circuit1.add_gate(std.RZX(1.0), targets=[1, 0]) + circuit1.add_gate(gates.CX, controls=0, targets=1) + circuit1.add_gate(gates.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) + circuit1.add_gate(gates.SWAP, targets=[0, 1]) + circuit1.add_gate(gates.ISWAP, targets=[2, 1]) + circuit1.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit1.add_gate(gates.SQRTSWAP, [2, 0]) + circuit1.add_gate(gates.SQRTISWAP, [0, 1]) + circuit1.add_gate(gates.SWAPALPHA(np.pi / 4), [1, 2]) + circuit1.add_gate( + gates.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0] + ) + circuit1.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit1.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit1.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit1.add_gate(gates.RZX(1.0), targets=[1, 0]) result1 = circuit1.run(init_state) circuit2 = QubitCircuit(3) - circuit2.add_gate(std.X, targets=1) - circuit2.add_gate(std.Y, targets=1) - circuit2.add_gate(std.Z, targets=2) - circuit2.add_gate(std.RX(np.pi / 4), targets=0) - circuit2.add_gate(std.RY(np.pi / 4), targets=0) - circuit2.add_gate(std.RZ(np.pi / 4), targets=1) - circuit2.add_gate(std.H, targets=0) - circuit2.add_gate(std.SQRTX, targets=0) - circuit2.add_gate(std.S, targets=2) - circuit2.add_gate(std.T, targets=1) - circuit2.add_gate(std.R([np.pi / 4, np.pi / 6]), targets=1) + circuit2.add_gate(gates.X, targets=1) + circuit2.add_gate(gates.Y, targets=1) + circuit2.add_gate(gates.Z, targets=2) + circuit2.add_gate(gates.RX(np.pi / 4), targets=0) + circuit2.add_gate(gates.RY(np.pi / 4), targets=0) + circuit2.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit2.add_gate(gates.H, targets=0) + circuit2.add_gate(gates.SQRTX, targets=0) + circuit2.add_gate(gates.S, targets=2) + circuit2.add_gate(gates.T, targets=1) + circuit2.add_gate(gates.R([np.pi / 4, np.pi / 6]), targets=1) circuit2.add_gate( - std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), + gates.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0, ) - circuit2.add_gate(std.CX, controls=0, targets=1) - circuit2.add_gate(std.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) - circuit2.add_gate(std.SWAP, targets=[0, 1]) - circuit2.add_gate(std.ISWAP, targets=[2, 1]) - circuit2.add_gate(std.CZ, controls=[0], targets=[2]) - circuit2.add_gate(std.SQRTSWAP, targets=[2, 0]) - circuit2.add_gate(std.SQRTISWAP, targets=[0, 1]) - circuit2.add_gate(std.SWAPALPHA(np.pi / 4), targets=[1, 2]) - circuit2.add_gate(std.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0]) - circuit2.add_gate(std.TOFFOLI, controls=[2, 0], targets=[1]) - circuit2.add_gate(std.FREDKIN, controls=[0], targets=[1, 2]) - circuit2.add_gate(std.BERKELEY, targets=[1, 0]) - circuit2.add_gate(std.RZX(arg_value=1.0), targets=[1, 0]) + circuit2.add_gate(gates.CX, controls=0, targets=1) + circuit2.add_gate(gates.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) + circuit2.add_gate(gates.SWAP, targets=[0, 1]) + circuit2.add_gate(gates.ISWAP, targets=[2, 1]) + circuit2.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit2.add_gate(gates.SQRTSWAP, targets=[2, 0]) + circuit2.add_gate(gates.SQRTISWAP, targets=[0, 1]) + circuit2.add_gate(gates.SWAPALPHA(np.pi / 4), targets=[1, 2]) + circuit2.add_gate( + gates.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0] + ) + circuit2.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit2.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit2.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit2.add_gate(gates.RZX(arg_value=1.0), targets=[1, 0]) result2 = circuit2.run(init_state) assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 GATES = [ - std.X, - std.Y, - std.Z, - std.H, - std.S, - std.Sdag, - std.T, - std.Tdag, - std.SWAP, - std.ISWAP, - std.ISWAPdag, - std.SQRTSWAP, - std.SQRTISWAPdag, - std.SQRTISWAP, - std.SQRTISWAPdag, - std.BERKELEY, - std.BERKELEYdag, + gates.X, + gates.Y, + gates.Z, + gates.H, + gates.S, + gates.Sdag, + gates.T, + gates.Tdag, + gates.SWAP, + gates.ISWAP, + gates.ISWAPdag, + gates.SQRTSWAP, + gates.SQRTISWAPdag, + gates.SQRTISWAP, + gates.SQRTISWAPdag, + gates.BERKELEY, + gates.BERKELEYdag, ] PARAMETRIC_GATE = [ - std.RX(0.5), - std.RY(0.5), - std.RZ(0.5), - std.PHASE(0.5), - std.R((0.5, 0.9)), - std.QASMU((0.1, 0.2, 0.3)), - std.SWAPALPHA(0.3), - std.MS((0.47, 0.8)), - std.RZX(0.6), + gates.RX(0.5), + gates.RY(0.5), + gates.RZ(0.5), + gates.PHASE(0.5), + gates.R((0.5, 0.9)), + gates.QASMU((0.1, 0.2, 0.3)), + gates.SWAPALPHA(0.3), + gates.MS((0.47, 0.8)), + gates.RZX(0.6), ] CONTROLLED_GATE = [ - std.CX, - std.CY, - std.CZ, - std.CH, - std.CS, - std.CT, - std.CRX(arg_value=0.7), - std.CRY(arg_value=0.88), - std.CRZ(arg_value=0.78), - std.CPHASE(arg_value=0.9), - std.CQASMU(arg_value=[0.9, 0.22, 0.15]), - std.TOFFOLI, - std.FREDKIN, + gates.CX, + gates.CY, + gates.CZ, + gates.CH, + gates.CS, + gates.CT, + gates.CRX(arg_value=0.7), + gates.CRY(arg_value=0.88), + gates.CRZ(arg_value=0.78), + gates.CPHASE(arg_value=0.9), + gates.CQASMU(arg_value=[0.9, 0.22, 0.15]), + gates.TOFFOLI, + gates.FREDKIN, ] diff --git a/tests/test_noise.py b/tests/test_noise.py index cf53b6211..a4bee8175 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -16,7 +16,7 @@ sigmam, ) from qutip_qip.device import Processor, SCQubits, LinearSpinChain -from qutip_qip.operations.std import X +from qutip_qip.operations.gates import X from qutip_qip.noise import ( RelaxationNoise, DecoherenceNoise, diff --git a/tests/test_optpulseprocessor.py b/tests/test_optpulseprocessor.py index 651ca20d4..1a0a408de 100644 --- a/tests/test_optpulseprocessor.py +++ b/tests/test_optpulseprocessor.py @@ -15,7 +15,7 @@ sigmay, identity, ) -from qutip_qip.operations.std import X, CX, H, SWAP +from qutip_qip.operations.gates import X, CX, H, SWAP class TestOptPulseProcessor: diff --git a/tests/test_phase_flip.py b/tests/test_phase_flip.py index 50428034f..55ca02a42 100644 --- a/tests/test_phase_flip.py +++ b/tests/test_phase_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import PhaseFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.std import Z +from qutip_qip.operations.gates import Z @pytest.fixture diff --git a/tests/test_processor.py b/tests/test_processor.py index 47ee32263..82c1cef4e 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -19,7 +19,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.device import Processor, LinearSpinChain from qutip_qip.operations import hadamard_transform -from qutip_qip.operations.std import ISWAP, X +from qutip_qip.operations.gates import ISWAP, X from qutip_qip.noise import DecoherenceNoise, RandomNoise, ControlAmpNoise from qutip_qip.pulse import Pulse from qutip_qip.qubits import qubit_states diff --git a/tests/test_qasm.py b/tests/test_qasm.py index b35f20267..7f7823527 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -8,7 +8,7 @@ from qutip_qip.circuit import QubitCircuit from qutip import tensor, rand_ket, basis, identity from qutip_qip.operations import Measurement, Gate -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates @pytest.mark.parametrize( @@ -86,8 +86,8 @@ def test_custom_gates(): qc = read_qasm(filepath) unitaries = qc.propagators() assert (unitaries[0] - unitaries[1]).norm() < 1e-12 - ry_cx = std.CX.get_qobj() * tensor( - identity(2), std.RY(np.pi / 2).get_qobj() + ry_cx = gates.CX.get_qobj() * tensor( + identity(2), gates.RY(np.pi / 2).get_qobj() ) assert (unitaries[2] - ry_cx).norm() < 1e-12 @@ -130,32 +130,32 @@ def test_qasm_str(): "x q[0];\nmeasure q[1] -> c[0]\n" ) simple_qc = QubitCircuit(2, num_cbits=1) - simple_qc.add_gate(std.X, targets=[0]) + simple_qc.add_gate(gates.X, targets=[0]) simple_qc.add_measurement("M", targets=[1], classical_store=0) assert circuit_to_qasm_str(simple_qc) == expected_qasm_str def test_export_import(): qc = QubitCircuit(3) - qc.add_gate(std.CRY(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(std.CRX(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(std.CRZ(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(std.CX, targets=1, controls=0) - qc.add_gate(std.TOFFOLI, targets=2, controls=[0, 1]) + qc.add_gate(gates.CRY(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(gates.CRX(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(gates.CRZ(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(gates.CX, targets=1, controls=0) + qc.add_gate(gates.TOFFOLI, targets=2, controls=[0, 1]) # qc.add_gate(SQRTX, targets=0) - qc.add_gate(std.CS, targets=1, controls=0) - qc.add_gate(std.CT, targets=1, controls=0) - qc.add_gate(std.SWAP, targets=[0, 1]) - qc.add_gate(std.QASMU(arg_value=[np.pi, np.pi, np.pi]), targets=[0]) - qc.add_gate(std.RX(np.pi), targets=[0]) - qc.add_gate(std.RY(np.pi), targets=[0]) - qc.add_gate(std.RZ(np.pi), targets=[0]) - qc.add_gate(std.H, targets=[0]) - qc.add_gate(std.X, targets=[0]) - qc.add_gate(std.Y, targets=[0]) - qc.add_gate(std.Z, targets=[0]) - qc.add_gate(std.S, targets=[0]) - qc.add_gate(std.T, targets=[0]) + qc.add_gate(gates.CS, targets=1, controls=0) + qc.add_gate(gates.CT, targets=1, controls=0) + qc.add_gate(gates.SWAP, targets=[0, 1]) + qc.add_gate(gates.QASMU(arg_value=[np.pi, np.pi, np.pi]), targets=[0]) + qc.add_gate(gates.RX(np.pi), targets=[0]) + qc.add_gate(gates.RY(np.pi), targets=[0]) + qc.add_gate(gates.RZ(np.pi), targets=[0]) + qc.add_gate(gates.H, targets=[0]) + qc.add_gate(gates.X, targets=[0]) + qc.add_gate(gates.Y, targets=[0]) + qc.add_gate(gates.Z, targets=[0]) + qc.add_gate(gates.S, targets=[0]) + qc.add_gate(gates.T, targets=[0]) # qc.add_gate(CZ, targets=[0], controls=[1]) # The generated code by default has a inclusion statement of @@ -233,7 +233,7 @@ def test_parsing_mode(tmp_path): ) propagator = circuit.compute_unitary() - fidelity = qutip.average_gate_fidelity(propagator, std.SWAP.get_qobj()) + fidelity = qutip.average_gate_fidelity(propagator, gates.SWAP.get_qobj()) pytest.approx(fidelity, 1.0) circuit = read_qasm( @@ -242,5 +242,5 @@ def test_parsing_mode(tmp_path): ) propagator = circuit.compute_unitary() - fidelity = qutip.average_gate_fidelity(propagator, std.SWAP.get_qobj()) + fidelity = qutip.average_gate_fidelity(propagator, gates.SWAP.get_qobj()) pytest.approx(fidelity, 1.0) diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 62fe32a7e..11cbfb8f2 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -15,7 +15,7 @@ CircularSpinChain, DispersiveCavityQED, ) -from qutip_qip.operations.std import X, CX, RX, H +from qutip_qip.operations.gates import X, CX, RX, H from qiskit import QuantumCircuit from qiskit_aer import AerSimulator diff --git a/tests/test_renderer.py b/tests/test_renderer.py index fd3ce806d..5b0c82dc3 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -4,7 +4,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer from qutip_qip.operations import controlled -from qutip_qip.operations.std import ( +from qutip_qip.operations.gates import ( IDLE, X, H, diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 948b0b1c7..f7d7eea80 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -4,7 +4,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.compiler import PulseInstruction, Scheduler from qutip_qip.operations import ControlledGate -from qutip_qip.operations.std import ( +from qutip_qip.operations.gates import ( GATE_CLASS_MAP, CX, X, diff --git a/tests/test_vqa.py b/tests/test_vqa.py index b57d98f3d..822b71956 100644 --- a/tests/test_vqa.py +++ b/tests/test_vqa.py @@ -2,7 +2,7 @@ import numpy as np import qutip from qutip_qip.operations import expand_operator -from qutip_qip.operations.std import H +from qutip_qip.operations.gates import H from qutip_qip.vqa import ( VQA, VQABlock, From b54d6c2f0d91a1222ab62cc100814abeeff9833a Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 18:07:56 +0530 Subject: [PATCH 053/117] Added __eq__, __hash__ to GateMetaclasses --- src/qutip_qip/operations/controlled.py | 10 +++---- src/qutip_qip/operations/gateclass.py | 39 +++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 26f86a81b..037bcec42 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -17,14 +17,12 @@ 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 class (e.g., CX.get_qobj()) + return partial(self.func, owner) - return partial( - self.func, instance - ) # Called on the instance (e.g., CRX(0.5).get_qobj()) + # Called on the instance (e.g., CRX(0.5).get_qobj()) + return partial(self.func, instance) class ControlledGate(Gate): diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index f1655d48b..28d78b171 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -80,11 +80,42 @@ class X(Gate): raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) - def __str__(self) -> str: - return f"Gate({self.name})" + def __str__(cls) -> str: + return f"Gate({cls.name})" + + def __repr__(cls) -> str: + return f"Gate({cls.name}, num_qubits={cls.num_qubits})" + + def __eq__(cls, other: any) -> bool: + # Return False if other is not a class. + if not isinstance(other, type): + return False + + if cls is other: + return True + + if isinstance(other, _GateMetaClass): + cls_name = getattr(cls, "name", None) + other_name = getattr(other, "name", None) + + cls_namespace = getattr(cls, "_namespace", "std") + other_namespace = getattr(other, "_namespace", "std") + + # They are equal if they share the same name and namespace + return cls_name == other_name and cls_namespace == other_namespace + + return False - def __repr__(self) -> str: - return f"Gate({self.name}, num_qubits={self.num_qubits})" + def __hash__(cls) -> int: + """ + Required because __eq__ is overridden. + Hashes the class based on its unique identity (namespace and name) + so it can still be safely used in the _registry sets and dicts. + """ + return hash(( + getattr(cls, "namespace", "std"), + getattr(cls, "name", None) + )) def clear_cache(cls, namespace: str): """ From c4fb1382d60bc293f95d0a6b948bc84155d8a2f6 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 20:56:15 +0530 Subject: [PATCH 054/117] Added caching single qubit parametrized gates --- .../operations/gates/single_qubit_gate.py | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 373c5d107..6e0ff1262 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -1,4 +1,5 @@ -from functools import cache +from functools import cache, lru_cache +from abc import abstractmethod import warnings import numpy as np @@ -19,6 +20,13 @@ class _SingleQubitParametricGate(AngleParametricGate): __slots__ = () num_qubits = 1 + @abstractmethod + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + pass + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + class X(_SingleQubitGate): """ @@ -372,8 +380,10 @@ class RX(_SingleQubitParametricGate): num_params = 1 latex_str = r"RX" - def get_qobj(self) -> Qobj: - phi = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] return Qobj( [ [np.cos(phi / 2), -1j * np.sin(phi / 2)], @@ -406,8 +416,10 @@ class RY(_SingleQubitParametricGate): num_params = 1 latex_str = r"RY" - def get_qobj(self) -> Qobj: - phi = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] return Qobj( [ [np.cos(phi / 2), -np.sin(phi / 2)], @@ -439,8 +451,10 @@ class RZ(_SingleQubitParametricGate): num_params = 1 latex_str = r"RZ" - def get_qobj(self) -> Qobj: - phi = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) def inverse(self) -> Gate: @@ -462,8 +476,10 @@ class PHASE(_SingleQubitParametricGate): num_params = 1 latex_str = r"PHASE" - def get_qobj(self) -> Qobj: - phi = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] return Qobj( [ [1, 0], @@ -502,8 +518,10 @@ class R(_SingleQubitParametricGate): num_params = 2 latex_str = r"{\rm R}" - def get_qobj(self) -> Qobj: - phi, theta = self.arg_value + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi, theta = arg_value return Qobj( [ [ @@ -544,8 +562,10 @@ class QASMU(_SingleQubitParametricGate): num_params = 3 latex_str = r"{\rm QASMU}" - def get_qobj(self) -> Qobj: - theta, phi, gamma = self.arg_value + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + theta, phi, gamma = arg_value return Qobj( [ [ From 91e025b29a4407e2f48ec9e3ba270de534941f16 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 21:15:23 +0530 Subject: [PATCH 055/117] Added caching for two qubit parametrized gates --- .../operations/gates/two_qubit_gate.py | 193 +++++++++++++----- 1 file changed, 145 insertions(+), 48 deletions(-) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 2ec79106e..acd87ef6e 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -1,5 +1,6 @@ from typing import Final -from functools import cache +from abc import abstractmethod +from functools import cache, lru_cache import warnings import numpy as np @@ -28,13 +29,22 @@ class _TwoQubitGate(Gate): num_qubits: Final[int] = 2 +class _TwoQubitParametricGate(AngleParametricGate): + """Abstract two-qubit Parametric Gate (non-controlled).""" + + __slots__ = () + num_qubits: Final[int] = 2 + + @abstractmethod + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + pass + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + + class _ControlledTwoQubitGate(ControlledGate): - """ - This class allows correctly generating the gate instance - when a redundant control_value is given, e.g. - ``CX``, - and raise an error if it is 0. - """ + """Abstract two-qubit Controlled Gate (both parametric and non-parametric).""" __slots__ = () num_qubits: Final[int] = 2 @@ -396,7 +406,7 @@ def inverse() -> Gate: return BERKELEY -class SWAPALPHA(AngleParametricGate): +class SWAPALPHA(_TwoQubitParametricGate): r""" SWAPALPHA gate. @@ -427,8 +437,10 @@ class SWAPALPHA(AngleParametricGate): num_params: int = 1 latex_str = r"{\rm SWAPALPHA}" - def get_qobj(self): - alpha = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + alpha = arg_value[0] return Qobj( [ [1, 0, 0, 0], @@ -454,7 +466,7 @@ def inverse(self) -> Gate: return SWAPALPHA(-alpha) -class MS(AngleParametricGate): +class MS(_TwoQubitParametricGate): r""" Mølmer–Sørensen gate. @@ -485,8 +497,10 @@ class MS(AngleParametricGate): num_params: int = 2 latex_str = r"{\rm MS}" - def get_qobj(self): - theta, phi = self.arg_value + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + theta, phi = arg_value return Qobj( [ [ @@ -512,7 +526,7 @@ def inverse(self) -> Gate: return MS([-theta, phi]) -class RZX(AngleParametricGate): +class RZX(_TwoQubitParametricGate): r""" RZX gate. @@ -543,8 +557,10 @@ class RZX(AngleParametricGate): num_params = 1 latex_str = r"{\rm RZX}" - def get_qobj(self): - theta = self.arg_value[0] + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + theta = arg_value[0] return Qobj( np.array( [ @@ -790,38 +806,6 @@ def get_qobj(): ) -class CPHASE(_ControlledTwoQubitGate): - r""" - CPHASE gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\theta} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations.gates import CPHASE - >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] - [0.+0.j 1.+0.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 1.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] - """ - - __slots__ = () - - num_params: int = 1 - target_gate = PHASE - latex_str = r"{\rm CPHASE}" - - class CRX(_ControlledTwoQubitGate): r""" Controlled X rotation. @@ -837,6 +821,22 @@ class CRX(_ControlledTwoQubitGate): target_gate = RX latex_str = r"{\rm CRX}" + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] + return Qobj( + [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(phi / 2), -1j * np.sin(phi / 2)], + [0, 0, -1j * np.sin(phi / 2), np.cos(phi / 2)] + ], + dims = [[2,2], [2,2]] + ) + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + class CRY(_ControlledTwoQubitGate): r""" @@ -853,6 +853,22 @@ class CRY(_ControlledTwoQubitGate): target_gate = RY latex_str = r"{\rm CRY}" + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] + return Qobj( + [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(phi / 2), -np.sin(phi / 2)], + [0, 0, np.sin(phi / 2), np.cos(phi / 2)] + ], + dims = [[2,2], [2,2]] + ) + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + class CRZ(_ControlledTwoQubitGate): r""" @@ -885,6 +901,70 @@ class CRZ(_ControlledTwoQubitGate): target_gate = RZ latex_str = r"{\rm CRZ}" + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] + return Qobj( + [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-1j * phi / 2), 0], + [0, 0, 0, np.exp(1j * phi / 2)] + ], + dims = [[2,2], [2,2]] + ) + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + + +class CPHASE(_ControlledTwoQubitGate): + r""" + CPHASE gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CPHASE + >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 1.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 1.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] + """ + + __slots__ = () + + num_params: int = 1 + target_gate = PHASE + latex_str = r"{\rm CPHASE}" + + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + phi = arg_value[0] + return Qobj( + [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, np.exp(1j * phi)] + ], + dims = [[2,2], [2,2]] + ) + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) + class CQASMU(_ControlledTwoQubitGate): r""" @@ -900,3 +980,20 @@ class CQASMU(_ControlledTwoQubitGate): num_params: int = 3 target_gate = QASMU latex_str = r"{\rm CQASMU}" + + @staticmethod + @lru_cache(maxsize=128) + def _compute_qobj(arg_value: tuple[float]) -> Qobj: + theta, phi, gamma = arg_value + return Qobj( + [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), + -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2)], + [0, 0, np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), + np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2)]] + , dims = [[2,2], [2,2]] + ) + + def get_qobj(self) -> Qobj: + return self._compute_qobj(tuple(self.arg_value)) From 6433f672a40843a5d82a972794d750d4be12e09e Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 1 Mar 2026 21:29:43 +0530 Subject: [PATCH 056/117] Added proper typehints for all standard gates Distinguish between Gates, Type[Gates] since non-paramaterized gates are just classes not objects. Similarly add the Final typehint for values which are not meant to change, it will be useful if in future we add a static typechecker --- src/qutip_qip/operations/gates/other_gates.py | 37 ++-- .../operations/gates/single_qubit_gate.py | 103 +++++------ .../operations/gates/two_qubit_gate.py | 160 +++++++++--------- 3 files changed, 156 insertions(+), 144 deletions(-) diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 6f78e2064..9a5025543 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -1,10 +1,11 @@ from functools import cache +from typing import Final, Type import scipy.sparse as sp import numpy as np from qutip import Qobj -from qutip_qip.operations import ControlledGate, AngleParametricGate +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate from qutip_qip.operations.gates import X, SWAP @@ -16,13 +17,13 @@ class GLOBALPHASE(AngleParametricGate): -------- >>> from qutip_qip.operations.gates import GLOBALPHASE """ - - num_qubits: int = 0 - num_params: int = 1 - self_inverse = False - latex_str = r"{\rm GLOBALPHASE}" __slots__ = () + num_qubits: Final[int] = 0 + num_params: Final[int] = 1 + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm GLOBALPHASE}" + def __init__(self, arg_value: float = 0.0): super().__init__(arg_value=arg_value) @@ -62,12 +63,14 @@ class TOFFOLI(ControlledGate): """ __slots__ = () - num_qubits: int = 3 - num_ctrl_qubits: int = 2 - ctrl_value = 0b11 - target_gate = X - latex_str = r"{\rm TOFFOLI}" + num_qubits: Final[int] = 3 + num_ctrl_qubits: Final[int] = 2 + ctrl_value: Final[int] = 0b11 + + target_gate: Final[Type[Gate]] = X + self_inverse: Final[bool] = True + latex_str: Final[str] = r"{\rm TOFFOLI}" @staticmethod @cache @@ -108,12 +111,14 @@ class FREDKIN(ControlledGate): """ __slots__ = () - num_qubits: int = 3 - num_ctrl_qubits: int = 1 - ctrl_value = 1 - target_gate = SWAP - latex_str = r"{\rm FREDKIN}" + num_qubits: Final[int] = 3 + num_ctrl_qubits: Final[int] = 1 + ctrl_value: Final[int] = 1 + + target_gate: Final[Type[Gate]] = SWAP + self_inverse: Final[bool] = True + latex_str: Final[str] = r"{\rm FREDKIN}" @staticmethod @cache diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 6e0ff1262..05809ad20 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -1,4 +1,5 @@ from functools import cache, lru_cache +from typing import Final, Type from abc import abstractmethod import warnings import numpy as np @@ -11,14 +12,14 @@ class _SingleQubitGate(Gate): """Abstract one-qubit gate.""" __slots__ = () - num_qubits = 1 + num_qubits: Final[int] = 1 class _SingleQubitParametricGate(AngleParametricGate): """Abstract one-qubit parametric gate.""" __slots__ = () - num_qubits = 1 + num_qubits: Final[int] = 1 @abstractmethod def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -44,9 +45,9 @@ class X(_SingleQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"X" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"X" @staticmethod @cache @@ -70,9 +71,9 @@ class Y(_SingleQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"Y" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"Y" @staticmethod @cache @@ -96,9 +97,9 @@ class Z(_SingleQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"Z" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"Z" @staticmethod @cache @@ -117,9 +118,9 @@ class IDLE(_SingleQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"{\rm IDLE}" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm IDLE}" @staticmethod @cache @@ -143,9 +144,9 @@ class H(_SingleQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"H" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"H" @staticmethod @cache @@ -186,9 +187,9 @@ class SQRTX(_SingleQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{\rm X}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{\rm X}" @staticmethod @cache @@ -196,7 +197,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTXdag @@ -216,9 +217,9 @@ class SQRTXdag(_SingleQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{\rm X}^\dagger" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{\rm X}^\dagger" @staticmethod @cache @@ -226,7 +227,7 @@ def get_qobj() -> Qobj: return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTX @@ -259,9 +260,9 @@ class S(_SingleQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"{\rm S}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm S}" @staticmethod @cache @@ -269,7 +270,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, 1j]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return Sdag @@ -289,9 +290,9 @@ class Sdag(_SingleQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"{\rm S^\dagger}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm S^\dagger}" @staticmethod @cache @@ -299,7 +300,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, -1j]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return S @@ -319,8 +320,8 @@ class T(_SingleQubitGate): __slots__ = () - self_inverse = False - latex_str = r"{\rm T}" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm T}" @staticmethod @cache @@ -328,7 +329,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return Tdag @@ -348,8 +349,8 @@ class Tdag(_SingleQubitGate): __slots__ = () - self_inverse = False - latex_str = r"{\rm Tdag}" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm Tdag}" @staticmethod @cache @@ -357,7 +358,7 @@ def get_qobj() -> Qobj: return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return T @@ -377,8 +378,8 @@ class RX(_SingleQubitParametricGate): __slots__ = () - num_params = 1 - latex_str = r"RX" + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_x" @staticmethod @lru_cache(maxsize=128) @@ -413,8 +414,8 @@ class RY(_SingleQubitParametricGate): __slots__ = () - num_params = 1 - latex_str = r"RY" + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_y" @staticmethod @lru_cache(maxsize=128) @@ -448,8 +449,8 @@ class RZ(_SingleQubitParametricGate): __slots__ = () - num_params = 1 - latex_str = r"RZ" + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_z" @staticmethod @lru_cache(maxsize=128) @@ -473,8 +474,8 @@ class PHASE(_SingleQubitParametricGate): __slots__ = () - num_params = 1 - latex_str = r"PHASE" + num_params: Final[int] = 1 + latex_str: Final[str] = r"PHASE" @staticmethod @lru_cache(maxsize=128) @@ -515,8 +516,8 @@ class R(_SingleQubitParametricGate): __slots__ = () - num_params = 2 - latex_str = r"{\rm R}" + num_params: Final[int] = 2 + latex_str: Final[str] = r"{\rm R}" @staticmethod @lru_cache(maxsize=128) @@ -559,8 +560,8 @@ class QASMU(_SingleQubitParametricGate): __slots__ = () - num_params = 3 - latex_str = r"{\rm QASMU}" + num_params: Final[int] = 3 + latex_str: Final[str] = r"{\rm QASMU}" @staticmethod @lru_cache(maxsize=128) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index acd87ef6e..75192f860 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -1,4 +1,4 @@ -from typing import Final +from typing import Final, Type from abc import abstractmethod from functools import cache, lru_cache import warnings @@ -70,12 +70,13 @@ class SWAP(_TwoQubitGate): __slots__ = () - self_inverse = True - is_clifford = True - latex_str = r"{\rm SWAP}" + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm SWAP}" @staticmethod - def get_qobj(): + @cache + def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dims=[[2, 2], [2, 2]], @@ -100,11 +101,12 @@ class ISWAP(_TwoQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"{i}{\rm SWAP}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{i}{\rm SWAP}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], @@ -112,7 +114,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return ISWAPdag @@ -134,11 +136,12 @@ class ISWAPdag(_TwoQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"{i}{\rm SWAP^\dagger}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{i}{\rm SWAP^\dagger}" @staticmethod + @cache def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, -1j, 0], [0, -1j, 0, 0], [0, 0, 0, 1]], @@ -146,7 +149,7 @@ def get_qobj() -> Qobj: ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return ISWAP @@ -168,10 +171,11 @@ class SQRTSWAP(_TwoQubitGate): __slots__ = () - self_inverse = False - latex_str = r"\sqrt{\rm SWAP}" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"\sqrt{\rm SWAP}" @staticmethod + @cache def get_qobj(): return Qobj( np.array( @@ -186,7 +190,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTSWAPdag @@ -208,10 +212,11 @@ class SQRTSWAPdag(_TwoQubitGate): __slots__ = () - self_inverse = False - latex_str = r"\sqrt{\rm SWAP}^\dagger" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"\sqrt{\rm SWAP}^\dagger" @staticmethod + @cache def get_qobj(): return Qobj( np.array( @@ -226,7 +231,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTSWAP @@ -248,11 +253,12 @@ class SQRTISWAP(_TwoQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{{i}\rm SWAP}" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{{i}\rm SWAP}" @staticmethod + @cache def get_qobj(): return Qobj( np.array( @@ -267,7 +273,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTISWAPdag @@ -289,11 +295,12 @@ class SQRTISWAPdag(_TwoQubitGate): __slots__ = () - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{{i}\rm SWAP}^\dagger" + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{{i}\rm SWAP}^\dagger" @staticmethod + @cache def get_qobj(): return Qobj( np.array( @@ -308,7 +315,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return SQRTISWAP @@ -339,10 +346,11 @@ class BERKELEY(_TwoQubitGate): __slots__ = () - self_inverse = False - latex_str = r"{\rm BERKELEY}" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm BERKELEY}" @staticmethod + @cache def get_qobj(): return Qobj( [ @@ -355,7 +363,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return BERKELEYdag @@ -386,10 +394,11 @@ class BERKELEYdag(_TwoQubitGate): __slots__ = () - self_inverse = False - latex_str = r"{\rm BERKELEY^\dagger}" + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm BERKELEY^\dagger}" @staticmethod + @cache def get_qobj(): return Qobj( [ @@ -402,7 +411,7 @@ def get_qobj(): ) @staticmethod - def inverse() -> Gate: + def inverse() -> Type[Gate]: return BERKELEY @@ -433,9 +442,8 @@ class SWAPALPHA(_TwoQubitParametricGate): __slots__ = () - num_qubits = 2 - num_params: int = 1 - latex_str = r"{\rm SWAPALPHA}" + num_params: Final[int] = 1 + latex_str: Final[str] = r"{\rm SWAPALPHA}" @staticmethod @lru_cache(maxsize=128) @@ -493,9 +501,8 @@ class MS(_TwoQubitParametricGate): __slots__ = () - num_qubits = 2 - num_params: int = 2 - latex_str = r"{\rm MS}" + num_params: Final[int] = 2 + latex_str: Final[str] = r"{\rm MS}" @staticmethod @lru_cache(maxsize=128) @@ -553,9 +560,8 @@ class RZX(_TwoQubitParametricGate): __slots__ = () - num_qubits = 2 - num_params = 1 - latex_str = r"{\rm RZX}" + num_params: Final[int] = 1 + latex_str: Final[str] = r"{\rm RZX}" @staticmethod @lru_cache(maxsize=128) @@ -596,9 +602,9 @@ class CX(_ControlledTwoQubitGate): __slots__ = () - target_gate = X - is_clifford = True - latex_str = r"{\rm CNOT}" + target_gate: Final[Type[Gate]] = X + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm CNOT}" @staticmethod @cache @@ -640,13 +646,13 @@ class CY(_ControlledTwoQubitGate): __slots__ = () - is_clifford = True - target_gate = Y - latex_str = r"{\rm CY}" + is_clifford: Final[bool] = True + target_gate: Final[Type[Gate]] = Y + latex_str: Final[str] = r"{\rm CY}" @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], dims=[[2, 2], [2, 2]], @@ -671,13 +677,13 @@ class CZ(_ControlledTwoQubitGate): __slots__ = () - target_gate = Z - is_clifford = True - latex_str = r"{\rm CZ}" + target_gate: Final[Type[Gate]] = Z + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm CZ}" @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], dims=[[2, 2], [2, 2]], @@ -717,12 +723,12 @@ class CH(_ControlledTwoQubitGate): __slots__ = () - target_gate = H - latex_str = r"{\rm CH}" + target_gate: Final[Type[Gate]] = H + latex_str: Final[str] = r"{\rm CH}" @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: sq_2 = 1 / np.sqrt(2) return Qobj( [ @@ -755,12 +761,12 @@ class CT(_ControlledTwoQubitGate): __slots__ = () - target_gate = T - latex_str = r"{\rm CT}" + target_gate: Final[Type[Gate]] = T + latex_str: Final[str] = r"{\rm CT}" @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [ [1, 0, 0, 0], @@ -792,12 +798,12 @@ class CS(_ControlledTwoQubitGate): __slots__ = () - target_gate = S - latex_str = r"{\rm CS}" + target_gate: Final[Type[Gate]] = S + latex_str: Final[str] = r"{\rm CS}" @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( np.array( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] @@ -817,9 +823,9 @@ class CRX(_ControlledTwoQubitGate): __slots__ = () - num_params: int = 1 - target_gate = RX - latex_str = r"{\rm CRX}" + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RX + latex_str: Final[str] = r"{\rm CRX}" @staticmethod @lru_cache(maxsize=128) @@ -849,9 +855,9 @@ class CRY(_ControlledTwoQubitGate): __slots__ = () - num_params: int = 1 - target_gate = RY - latex_str = r"{\rm CRY}" + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RY + latex_str: Final[str] = r"{\rm CRY}" @staticmethod @lru_cache(maxsize=128) @@ -897,9 +903,9 @@ class CRZ(_ControlledTwoQubitGate): __slots__ = () - num_params: int = 1 - target_gate = RZ - latex_str = r"{\rm CRZ}" + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RZ + latex_str: Final[str] = r"{\rm CRZ}" @staticmethod @lru_cache(maxsize=128) @@ -945,9 +951,9 @@ class CPHASE(_ControlledTwoQubitGate): __slots__ = () - num_params: int = 1 - target_gate = PHASE - latex_str = r"{\rm CPHASE}" + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = PHASE + latex_str: Final[str] = r"{\rm CPHASE}" @staticmethod @lru_cache(maxsize=128) @@ -977,9 +983,9 @@ class CQASMU(_ControlledTwoQubitGate): __slots__ = () - num_params: int = 3 - target_gate = QASMU - latex_str = r"{\rm CQASMU}" + num_params: Final[int] = 3 + target_gate: Final[Type[Gate]] = QASMU + latex_str: Final[str] = r"{\rm CQASMU}" @staticmethod @lru_cache(maxsize=128) From dc702df4986817f3da0f8fca2184f6cd5cbd1ef9 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 00:42:19 +0530 Subject: [PATCH 057/117] Removed arg_value from ParametricGate Made arg_value a property and instead defined theta for RX, (theta, phi, gamma) in __init__ for QASMU. This allows for each standard gate to determine its parameters --- src/qutip_qip/operations/controlled.py | 2 +- src/qutip_qip/operations/gates/other_gates.py | 13 ++-- .../operations/gates/single_qubit_gate.py | 59 +++++++++++++------ .../operations/gates/two_qubit_gate.py | 27 ++++++--- src/qutip_qip/operations/old_gates.py | 2 +- src/qutip_qip/operations/parametric.py | 23 ++++---- 6 files changed, 81 insertions(+), 45 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 037bcec42..338cbc8d2 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -200,7 +200,7 @@ def inverse(cls_or_self) -> Gate: cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value, ) - return inverse(arg_value=arg_value) + return inverse(*arg_value) @staticmethod def is_controlled() -> bool: diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 9a5025543..84ea955c2 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -17,26 +17,27 @@ class GLOBALPHASE(AngleParametricGate): -------- >>> from qutip_qip.operations.gates import GLOBALPHASE """ - __slots__ = () + __slots__ = ("phase") num_qubits: Final[int] = 0 num_params: Final[int] = 1 self_inverse: Final[bool] = False latex_str: Final[str] = r"{\rm GLOBALPHASE}" - def __init__(self, arg_value: float = 0.0): - super().__init__(arg_value=arg_value) + def __init__(self, phase: float = 0.0): + super().__init__(phase) + self.phase = phase def __repr__(self): - return f"Gate({self.name}, phase {self.arg_value})" + return f"Gate({self.name}, phase {self.phase})" def get_qobj(self, num_qubits=None): if num_qubits is None: - return Qobj(self.arg_value) + return Qobj(self.phase) N = 2**num_qubits return Qobj( - np.exp(1.0j * self.arg_value[0]) + np.exp(1.0j * self.phase) * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], ) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 05809ad20..1a10eb62a 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -376,11 +376,15 @@ class RX(_SingleQubitParametricGate): [0. -0.70711j 0.70711+0.j ]] """ - __slots__ = () + __slots__ = ("theta") num_params: Final[int] = 1 latex_str: Final[str] = r"R_x" + def __init__(self, theta: float, arg_label = None): + super().__init__(theta, arg_label=arg_label) + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -394,8 +398,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - theta = self.arg_value[0] - return RX(-theta) + return RX(theta=-self.theta) class RY(_SingleQubitParametricGate): @@ -412,11 +415,15 @@ class RY(_SingleQubitParametricGate): [ 0.70711 0.70711]] """ - __slots__ = () + __slots__ = ("theta") num_params: Final[int] = 1 latex_str: Final[str] = r"R_y" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -429,8 +436,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - theta = self.arg_value[0] - return RY(-theta) + return RY(theta=-self.theta) class RZ(_SingleQubitParametricGate): @@ -447,11 +453,15 @@ class RZ(_SingleQubitParametricGate): [0. +0.j 0.70711+0.70711j]] """ - __slots__ = () + __slots__ = ("theta") num_params: Final[int] = 1 latex_str: Final[str] = r"R_z" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -459,8 +469,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) def inverse(self) -> Gate: - theta = self.arg_value[0] - return RZ(-theta) + return RZ(theta=-self.theta) class PHASE(_SingleQubitParametricGate): @@ -472,11 +481,15 @@ class PHASE(_SingleQubitParametricGate): >>> from qutip_qip.operations.gates import PHASE """ - __slots__ = () + __slots__ = ("theta") num_params: Final[int] = 1 latex_str: Final[str] = r"PHASE" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -489,8 +502,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - phi = self.arg_value[0] - return PHASE(-phi) + return PHASE(theta=-self.theta) class R(_SingleQubitParametricGate): @@ -507,18 +519,23 @@ class R(_SingleQubitParametricGate): Examples -------- >>> from qutip_qip.operations.gates import R - >>> R((np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + >>> R(np.pi/2, np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False Qobj data = [[ 0.70711 -0.70711] [ 0.70711 0.70711]] """ - __slots__ = () + __slots__ = ("theta", "phi") num_params: Final[int] = 2 latex_str: Final[str] = r"{\rm R}" + def __init__(self, phi: float, theta: float, arg_label: str | None = None): + super().__init__(phi, theta, arg_label=arg_label) + self.phi = phi + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -537,8 +554,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - phi, theta = self.arg_value - return R([phi, -theta]) + return R(phi=self.phi, theta=-self.theta) class QASMU(_SingleQubitParametricGate): @@ -558,11 +574,17 @@ class QASMU(_SingleQubitParametricGate): [ 0.5+0.5j -0.5+0.5j]] """ - __slots__ = () + __slots__ = ("theta", "phi", "gamma") num_params: Final[int] = 3 latex_str: Final[str] = r"{\rm QASMU}" + def __init__(self, theta: float, phi: float, gamma: float, arg_label: str | None = None): + super().__init__(theta, phi, gamma, arg_label=arg_label) + self.theta = theta + self.phi = phi + self.gamma = gamma + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -581,5 +603,4 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - theta, phi, gamma = self.arg_value - return QASMU([-theta, -gamma, -phi]) + return QASMU(-self.theta, -self.gamma, -self.phi) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 75192f860..b4faea6c7 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -440,11 +440,15 @@ class SWAPALPHA(_TwoQubitParametricGate): [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] """ - __slots__ = () + __slots__ = ("alpha") num_params: Final[int] = 1 latex_str: Final[str] = r"{\rm SWAPALPHA}" + def __init__(self, alpha: float, arg_label: str | None = None): + super().__init__(alpha, arg_label=arg_label) + self.alpha = alpha + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -470,8 +474,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - alpha = self.arg_value[0] - return SWAPALPHA(-alpha) + return SWAPALPHA(alpha=-self.alpha) class MS(_TwoQubitParametricGate): @@ -499,11 +502,16 @@ class MS(_TwoQubitParametricGate): [0. -0.70711j 0. +0.j 0. +0.j 0.70711+0.j ]] """ - __slots__ = () + __slots__ = ("theta", "phi") num_params: Final[int] = 2 latex_str: Final[str] = r"{\rm MS}" + def __init__(self, theta: float, phi: float, arg_label: str | None = None): + super().__init__(theta, phi, arg_label=arg_label) + self.theta = theta + self.phi = phi + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -529,8 +537,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - theta, phi = self.arg_value - return MS([-theta, phi]) + return MS(theta=-self.theta, phi=self.phi) class RZX(_TwoQubitParametricGate): @@ -558,11 +565,15 @@ class RZX(_TwoQubitParametricGate): [0.+0.j 0.+0.j 0.+1.j 0.+0.j]] """ - __slots__ = () + __slots__ = ("theta") num_params: Final[int] = 1 latex_str: Final[str] = r"{\rm RZX}" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + self.theta = theta + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: @@ -581,7 +592,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: def inverse(self) -> Gate: theta = self.arg_value[0] - return RZX(-theta) + return RZX(theta=-theta) class CX(_ControlledTwoQubitGate): diff --git a/src/qutip_qip/operations/old_gates.py b/src/qutip_qip/operations/old_gates.py index dcc314364..8eb16c5a1 100644 --- a/src/qutip_qip/operations/old_gates.py +++ b/src/qutip_qip/operations/old_gates.py @@ -456,7 +456,7 @@ def qasmu_gate(args, N=None, target=0): """ warnings.warn( "qasmu_gate has been deprecated and will be removed in future version. \ - Use QASMU([theta, phi, gamma]).get_qobj() instead.", + Use QASMU(theta, phi, gamma).get_qobj() instead.", DeprecationWarning, stacklevel=2, ) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 574d6f124..0700d6b7c 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -42,7 +42,7 @@ class attribute for subclasses. If the number of provided arguments does not match `num_params`. """ - __slots__ = ("arg_value", "arg_label") + __slots__ = ("_arg_value", "arg_label") num_params: int def __init_subclass__(cls, **kwargs) -> None: @@ -63,19 +63,19 @@ def __init_subclass__(cls, **kwargs) -> None: f"got {type(num_params)} with value {num_params}." ) - def __init__(self, arg_value: float, arg_label: str | None = None): - if not isinstance(arg_value, Iterable): - arg_value = [arg_value] - - if len(arg_value) != self.num_params: + def __init__(self, *args, arg_label: str | None = None): + if len(args) != self.num_params: raise ValueError( - f"Requires {self.num_params} parameters, got {len(arg_value)}" + f"Requires {self.num_params} parameters, got {len(args)}" ) - - self.validate_params(arg_value) - self.arg_value = list(arg_value) + self.validate_params(args) + self._arg_value = args self.arg_label = arg_label + @property + def arg_value(self) -> tuple[any]: + return self._arg_value + @staticmethod @abstractmethod def validate_params(arg_value): @@ -122,6 +122,9 @@ def __eq__(self, other) -> bool: return False return True + def __hash__(self) -> None: + pass + class AngleParametricGate(ParametricGate): __slots__ = () From e84da96a14d7402fd46ad8ef2bd650c70111b330 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 00:44:10 +0530 Subject: [PATCH 058/117] Made the remaining tests pass based on removal of arg_value --- src/qutip_qip/algorithms/qft.py | 8 +- src/qutip_qip/circuit/_decompose.py | 126 +++++++++--------- src/qutip_qip/circuit/circuit.py | 24 ++-- .../circuit/simulator/matrix_mul_simulator.py | 3 + src/qutip_qip/compiler/cavityqedcompiler.py | 4 +- src/qutip_qip/compiler/circuitqedcompiler.py | 10 +- src/qutip_qip/compiler/spinchaincompiler.py | 5 +- .../decompose/decompose_single_qubit_gate.py | 28 ++-- src/qutip_qip/device/cavityqed.py | 4 +- src/qutip_qip/device/circuitqed.py | 4 +- src/qutip_qip/device/spinchain.py | 13 +- src/qutip_qip/qasm.py | 24 ++-- src/qutip_qip/qiskit/utils/converter.py | 2 +- tests/test_circuit.py | 18 +-- tests/test_compiler.py | 14 +- tests/test_device.py | 8 +- tests/test_gates.py | 57 ++++---- tests/test_qasm.py | 10 +- tests/test_renderer.py | 10 +- 19 files changed, 181 insertions(+), 191 deletions(-) diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index c9e989923..e1df57a3f 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -67,7 +67,7 @@ def qft_steps(N=1, swapping=True): for j in range(i): U_step_list.append( expand_operator( - CPHASE(arg_value=np.pi / (2 ** (i - j))).get_qobj(), + CPHASE(theta=np.pi / (2 ** (i - j))).get_qobj(), dims=[2] * N, targets=[i, j], ) @@ -115,7 +115,7 @@ def qft_gate_sequence(N=1, swapping=True, to_cnot=False): if not to_cnot: qc.add_gate( CPHASE( - arg_value=np.pi / (2 ** (i - j)), + theta=np.pi / (2 ** (i - j)), arg_label=r"{\pi/2^{%d}}" % (i - j), ), targets=[j], @@ -140,6 +140,6 @@ def _cphase_to_cnot(targets, controls, arg_value, qc: QubitCircuit): qc.add_gate(decomposed_gates[4], targets=targets) qc.add_gate(CX, targets=targets, controls=controls) qc.add_gate(RZ(arg_value / 2), targets=controls) - gate = decomposed_gates[7] - gate.arg_value[0] += arg_value / 4 + gate = decomposed_gates[7] # This is a GLOBALPHASE Gate + gate.phase += arg_value / 4 qc.add_gate(gate, targets=targets) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 9ecdc998f..2c0daf404 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -62,7 +62,7 @@ def _gate_SQRTNOT(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 4) temp_resolved.add_gate( - RX(arg_value=np.pi / 2, arg_label=r"\pi/2"), + RX(np.pi / 2, arg_label=r"\pi/2"), targets=targets, ) @@ -73,11 +73,11 @@ def _gate_H(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=half_pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets, ) @@ -91,7 +91,7 @@ def _gate_PHASEGATE(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=gate.arg_value / 2) temp_resolved.add_gate( - gate=RZ(arg_value=gate.arg_value, arg_label=gate.arg_label), + gate=RZ(gate.arg_value, arg_label=gate.arg_label), targets=targets, ) @@ -103,20 +103,20 @@ def _gate_CZ(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets, ) @@ -141,28 +141,28 @@ def _gate_ISWAP(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, targets=targets[1], controls=targets[0]) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), + RZ(half_pi, arg_label=r"\pi/2"), targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), + RZ(half_pi, arg_label=r"\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets[0], ) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets[0], ) @@ -174,72 +174,72 @@ def _gate_FREDKIN(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), + RZ(pi, arg_label=r"\pi"), targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), + RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), + RZ(-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), + RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), + RZ(pi, arg_label=r"\pi"), targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + RZ(-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + RZ(pi / 4, arg_label=r"\pi/4"), targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + RZ(pi / 4, arg_label=r"\pi/4"), targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + RZ(-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), + RZ(pi / 4, arg_label=r"\pi/4"), targets=controls, ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), + RZ(-pi / 4, arg_label=r"-\pi/4"), targets=targets[0], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( - gate=RZ(arg_value=-3 * pi / 4, arg_label=r"-3\pi/4"), + gate=RZ(-3 * pi / 4, arg_label=r"-3\pi/4"), targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1] + RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1] ) temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), + RZ(-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), + RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), + RZ(pi, arg_label=r"\pi"), targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) @@ -256,57 +256,57 @@ def _gate_TOFFOLI(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 8) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), + RZ(half_pi, arg_label=r"\pi/2"), targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + RZ(quarter_pi, arg_label=r"\pi/4"), targets=controls[0], ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + RZ(-quarter_pi, arg_label=r"-\pi/4"), targets=controls[1], ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets, ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + RZ(quarter_pi, arg_label=r"\pi/4"), targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + RZ(quarter_pi, arg_label=r"\pi/4"), targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + RZ(-quarter_pi, arg_label=r"-\pi/4"), targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), + RZ(quarter_pi, arg_label=r"\pi/4"), targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + RZ(-quarter_pi, arg_label=r"-\pi/4"), targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), + RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), + RX(np.pi, arg_label=r"\pi"), targets=targets, ) temp_resolved.add_global_phase(phase=np.pi) @@ -321,12 +321,12 @@ def _basis_CZ(qc_temp, temp_resolved): if gate.name == "CNOT" or gate.name == "CX": qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate(CZ, targets=targets, controls=controls) qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) else: @@ -355,24 +355,24 @@ def _basis_ISWAP(qc_temp, temp_resolved): qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RZ(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RZ(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RZ(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RZ(half_pi, arg_label=r"\pi/2"), targets=targets, ) @@ -380,17 +380,17 @@ def _basis_ISWAP(qc_temp, temp_resolved): qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[0], ) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[1], ) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[0], ) @@ -414,25 +414,25 @@ def _basis_SQRTSWAP(qc_temp, temp_resolved): if gate.name == "CNOT" or gate.name == "CX": qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) qc_temp.add_gate(SQRTSWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=np.pi, arg_label=r"\pi"), + gate=RZ(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_gate(SQRTSWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) else: @@ -455,29 +455,29 @@ def _basis_SQRTISWAP(qc_temp, temp_resolved): if gate.name == "CNOT" or gate.name == "CX": qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RX(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RX(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate(SQRTISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RX(arg_value=np.pi, arg_label=r"\pi"), + gate=RX(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_gate(SQRTISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RZ(arg_value=np.pi, arg_label=r"\pi"), + gate=RZ(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_global_phase(phase=7 / 4 * np.pi) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 2ad3b9980..e2f5c0b3a 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -250,7 +250,7 @@ def add_gate( gate: Gate | str, targets: int | Iterable[int] = (), controls: int | Iterable[int] = (), - arg_value: any = None, + arg_value: None = None, arg_label: str | None = None, control_value: None = None, classical_controls: int | Iterable[int] = (), @@ -292,7 +292,7 @@ def add_gate( if arg_value is not None or arg_label is not None: warnings.warn( - "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(arg_value=np.pi)" + "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(np.pi)" ", 'arg_value', 'arg_label' arguments will be removed from 'add_gate' method in the future version.", DeprecationWarning, stacklevel=2, @@ -357,7 +357,7 @@ def add_gate( ) if gate_class.is_parametric(): - gate = gate_class(arg_value=arg_value, arg_label=arg_label) + gate = gate_class(arg_value, arg_label=arg_label) else: gate = gate_class @@ -716,43 +716,43 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): if gate.name == "RX" and "RX" not in basis_1q: qc_temp.add_gate( - RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RZ(arg_value=gate.arg_value, arg_label=gate.arg_label), + RZ(gate.theta, arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RY(arg_value=-half_pi, arg_label=r"\pi/2"), + RY(-half_pi, arg_label=r"\pi/2"), targets=targets, ) elif gate.name == "RY" and "RY" not in basis_1q: qc_temp.add_gate( - RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RX(arg_value=gate.arg_value, arg_label=gate.arg_label), + RX(gate.theta, arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), + RZ(half_pi, arg_label=r"\pi/2"), targets=targets, ) elif gate.name == "RZ" and "RZ" not in basis_1q: qc_temp.add_gate( - RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RY(arg_value=gate.arg_value, arg_label=gate.arg_label), + RY(gate.theta, arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RX(arg_value=half_pi, arg_label=r"\pi/2"), + RX(half_pi, arg_label=r"\pi/2"), targets=targets, ) else: diff --git a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py index 5869e067a..e07c61ec9 100644 --- a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py +++ b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py @@ -255,6 +255,9 @@ def step(self): else: state = self._evolve_state(gate, qubits, current_state) + else: + raise ValueError(f"Invalid operation {self.qc.instructions[self._op_index]}") + self._state = state self._op_index += 1 diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 457aa5a5a..98f28c5f8 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -195,7 +195,7 @@ def _swap_compiler( # corrections compiled_gate1 = self.gate_compiler["RZ"]( GateInstruction( - operation=RZ(arg_value=correction_angle), qubits=(q1,) + operation=RZ(correction_angle), qubits=(q1,) ), args, ) @@ -203,7 +203,7 @@ def _swap_compiler( compiled_gate2 = self.gate_compiler["RZ"]( GateInstruction( - operation=RZ(arg_value=correction_angle), qubits=(q2,) + operation=RZ(correction_angle), qubits=(q2,) ), args, ) diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 30abc9b5c..03ca217fd 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -283,27 +283,27 @@ def cnot_compiler(self, circuit_instruction, args): # += extends a list in Python result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=-PI / 2), qubits=(q2,)), + GateInstruction(operation=RX(-PI / 2), qubits=(q2,)), args, ) result += self.gate_compiler["RZX"]( - GateInstruction(operation=RZX(arg_value=PI / 2), qubits=(q1, q2)), + GateInstruction(operation=RZX(PI / 2), qubits=(q1, q2)), args, ) result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=-PI / 2), qubits=(q1,)), + GateInstruction(operation=RX(-PI / 2), qubits=(q1,)), args, ) result += self.gate_compiler["RY"]( - GateInstruction(operation=RY(arg_value=-PI / 2), qubits=(q1,)), + GateInstruction(operation=RY(-PI / 2), qubits=(q1,)), args, ) result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=PI / 2), qubits=(q1,)), + GateInstruction(operation=RX(PI / 2), qubits=(q1,)), args, ) diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index fa431d9ff..4789a69b0 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -71,10 +71,11 @@ class SpinChainCompiler(GateCompiler): >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import ModelProcessor, SpinChainModel >>> from qutip_qip.compiler import SpinChainCompiler + >>> from qutip_qip.operations.gates import RX, RZ >>> >>> qc = QubitCircuit(2) - >>> qc.add_gate("RX", targets=0, arg_value=np.pi) - >>> qc.add_gate("RZ", targets=1, arg_value=np.pi) + >>> qc.add_gate(RX(np.pi), targets=0) + >>> qc.add_gate(RZ(np.pi), targets=1) >>> >>> model = SpinChainModel(2, "linear", g=0.1) >>> processor = ModelProcessor(model=model) diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index d132d5a74..ab653dcf5 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -58,17 +58,17 @@ def _ZYZ_rotation(input_gate): check_gate(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_beta = RZ( - arg_value=beta, + beta, arg_label=rf"{(beta / np.pi):0.2f} \times \pi", ) Ry_theta = RY( - arg_value=theta, + theta, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Rz_alpha = RZ( - arg_value=alpha, + alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) @@ -90,17 +90,17 @@ def _ZXZ_rotation(input_gate): beta = beta + np.pi / 2 # theta and global phase are same as ZYZ values - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_alpha = RZ( - arg_value=alpha, + alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi".format, ) Rx_theta = RX( - arg_value=theta, + theta, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Rz_beta = RZ( - arg_value=beta, + beta, arg_label=rf"{(beta / np.pi):0.2f} \times \pi", ) @@ -116,26 +116,26 @@ def _ZYZ_pauli_X(input_gate): check_gate(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_A = RZ( - arg_value=alpha, +alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) Ry_A = RY( - arg_value=theta / 2, + theta / 2, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Pauli_X = X Ry_B = RY( - arg_value=-theta / 2, + -theta / 2, arg_label=rf"{(-theta / np.pi):0.2f} \times \pi", ) Rz_B = RZ( - arg_value=-(alpha + beta) / 2, + -(alpha + beta) / 2, arg_label=rf"{(-(alpha + beta) / (2 * np.pi)):0.2f} \times \pi", ) Rz_C = RZ( - arg_value=(-alpha + beta) / 2, + (-alpha + beta) / 2, arg_label=rf"{((-alpha + beta) / (2 * np.pi)):0.2f} \times \pi", ) diff --git a/src/qutip_qip/device/cavityqed.py b/src/qutip_qip/device/cavityqed.py index 3427116e0..4a6b30a26 100644 --- a/src/qutip_qip/device/cavityqed.py +++ b/src/qutip_qip/device/cavityqed.py @@ -55,8 +55,8 @@ class DispersiveCavityQED(ModelProcessor): from qutip_qip.device import DispersiveCavityQED qc = QubitCircuit(2) - qc.add_gate(RX(arg_value=np.pi), targets=0) - qc.add_gate(RY(arg_value=np.pi), targets=1) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(ISWAP, targets=[1, 0]) processor = DispersiveCavityQED(2, g=0.1) diff --git a/src/qutip_qip/device/circuitqed.py b/src/qutip_qip/device/circuitqed.py index 45e932ec1..6f707bdf8 100644 --- a/src/qutip_qip/device/circuitqed.py +++ b/src/qutip_qip/device/circuitqed.py @@ -49,8 +49,8 @@ class SCQubits(ModelProcessor): from qutip_qip.operations.gates import RY, RZ, CX qc = QubitCircuit(2) - qc.add_gate(RZ, targets=0, arg_value=np.pi) - qc.add_gate(RY, targets=1, arg_value=np.pi) + qc.add_gate(RZ(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(CX, targets=0, controls=1) processor = SCQubits(2) diff --git a/src/qutip_qip/device/spinchain.py b/src/qutip_qip/device/spinchain.py index 1c15b6534..38718c5ef 100644 --- a/src/qutip_qip/device/spinchain.py +++ b/src/qutip_qip/device/spinchain.py @@ -124,11 +124,12 @@ class LinearSpinChain(SpinChain): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import LinearSpinChain + from qutip_qip.operations.gates import RX, RY, ISWAP qc = QubitCircuit(2) - qc.add_gate("RX", targets=0, arg_value=np.pi) - qc.add_gate("RY", targets=1, arg_value=np.pi) - qc.add_gate("ISWAP", targets=[1, 0]) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) + qc.add_gate(ISWAP, targets=[1, 0]) processor = LinearSpinChain(2, g=0.1, t1=300) processor.load_circuit(qc) @@ -208,11 +209,11 @@ class CircularSpinChain(SpinChain): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import CircularSpinChain - from qutip_qip.operations import RX, RY, ISWAP + from qutip_qip.operations.gates import RX, RY, ISWAP qc = QubitCircuit(2) - qc.add_gate(RX(arg_value=np.pi), targets=0) - qc.add_gate(RY(arg_value=np.pi), targets=1) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(ISWAP, targets=[1, 0]) processor = CircularSpinChain(2, g=0.1, t1=300) diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 60ac98991..a6ff8414e 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -2,12 +2,13 @@ import re import os +import warnings from itertools import chain from copy import deepcopy -import warnings +from collections.abc import Iterable +from math import pi # Don't remove import numpy as np -from math import pi # Don't remove from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import unitary_gate @@ -494,7 +495,7 @@ def _regs_processor(self, regs, reg_type): *list( map( lambda x: ( - x if isinstance(x, list) else [x] * expand + x if isinstance(x, Iterable) else [x] * expand ), new_regs, ) @@ -553,15 +554,14 @@ def _add_qiskit_gates( if name == "u3": qc.add_gate( - gates.QASMU(args), + gates.QASMU(*args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "u2": - u2_args = [np.pi / 2, args[0], args[1]] qc.add_gate( - gates.QASMU(u2_args), + gates.QASMU(np.pi / 2, args[0], args[1]), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -621,7 +621,7 @@ def _add_qiskit_gates( ) elif name == "crz": qc.add_gate( - gates.CRZ(arg_value=args), + gates.CRZ(args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -629,7 +629,7 @@ def _add_qiskit_gates( ) elif name == "cu1": qc.add_gate( - gates.CPHASE(arg_value=args), + gates.CPHASE(args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -637,7 +637,7 @@ def _add_qiskit_gates( ) elif name == "cu3": qc.add_gate( - gates.CQASMU(arg_value=args), + gates.CQASMU(*args), controls=regs[0], targets=[regs[1]], classical_controls=classical_controls, @@ -723,7 +723,7 @@ def _add_predefined_gates( ) elif name == "U": qc.add_gate( - gates.QASMU(arg_value=[float(arg) for arg in com_args]), + gates.QASMU(*com_args), targets=int(com_regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -799,7 +799,7 @@ def _gate_add( classical_control_value=classical_control_value, ) else: - if not isinstance(regs, list): + if not isinstance(regs, Iterable): regs = [regs] if custom_gate_unitary is not None: @@ -1000,7 +1000,7 @@ def _qasm_str(self, q_name, q_targets, q_controls=None, q_args=None): q_regs = ",".join(q_regs) if q_args: - if isinstance(q_args, list): + if isinstance(q_args, Iterable): q_args = ",".join([str(arg) for arg in q_args]) return "{}({}) {};".format(q_name, q_args, q_regs) else: diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index 96fd99655..64204da53 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -119,7 +119,7 @@ def convert_qiskit_circuit_to_qutip( if qiskit_instruction.name in _map_gates.keys(): gate = _map_gates[qiskit_instruction.name] if gate.is_parametric(): - gate = gate(arg_value) + gate = gate(*arg_value) qutip_circuit.add_gate( gate, diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 4ba8ea0c8..3f012949a 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -131,9 +131,9 @@ def test_add_gate(self): qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) qc.add_gate(gates.H, targets=[3]) qc.add_gate(gates.SWAP, targets=[1, 4]) - qc.add_gate(gates.RY(arg_value=1.570796), targets=4) - qc.add_gate(gates.RY(arg_value=1.570796), targets=5) - qc.add_gate(gates.RX(arg_value=-1.570796), targets=[3]) + qc.add_gate(gates.RY(np.pi/2), targets=4) + qc.add_gate(gates.RY(np.pi/2), targets=5) + qc.add_gate(gates.RX(-np.pi/2), targets=[3]) # Test explicit gate addition assert qc.instructions[0].operation.name == "CX" @@ -151,15 +151,15 @@ def test_add_gate(self): # Test adding 1 qubit gate on [start, end] qubits assert qc.instructions[5].operation.name == "RY" assert qc.instructions[5].targets == (4,) - assert qc.instructions[5].operation.arg_value[0] == 1.570796 + assert qc.instructions[5].operation.arg_value[0] == np.pi/2 assert qc.instructions[6].operation.name == "RY" assert qc.instructions[6].targets == (5,) - assert qc.instructions[6].operation.arg_value[0] == 1.570796 + assert qc.instructions[6].operation.arg_value[0] == np.pi/2 # Test adding 1 qubit gate on qubits [3] assert qc.instructions[7].operation.name == "RX" assert qc.instructions[7].targets == (3,) - assert qc.instructions[7].operation.arg_value[0] == -1.570796 + assert qc.instructions[7].operation.arg_value[0] == -np.pi/2 class DUMMY1(Gate): num_qubits = 1 @@ -209,7 +209,7 @@ def test_add_circuit(self): qc.add_measurement("M0", targets=[0], classical_store=[1]) qc.add_gate(gates.RY(1.570796), targets=4) qc.add_gate(gates.RY(1.570796), targets=5) - qc.add_gate(gates.CRX(arg_value=np.pi / 2), controls=[1], targets=[2]) + qc.add_gate(gates.CRX(np.pi / 2), controls=[1], targets=[2]) qc1 = QubitCircuit(6) qc1.add_circuit(qc) @@ -382,7 +382,7 @@ def test_reverse(self): """ qc = QubitCircuit(3, num_cbits=1) - qc.add_gate(gates.RX(arg_value=3.141, arg_label=r"\pi/2"), targets=[0]) + qc.add_gate(gates.RX(3.141, arg_label=r"\pi/2"), targets=[0]) qc.add_gate(gates.CX, targets=[1], controls=[0]) qc.add_measurement("M1", targets=[1], classical_store=0) qc.add_gate(gates.H, targets=[2]) @@ -427,7 +427,7 @@ def get_qobj(): return Qobj(mat, dims=[[2], [2]]) qc = QubitCircuit(3) - qc.add_gate(gates.CRX(arg_value=np.pi / 2), targets=[2], controls=[1]) + qc.add_gate(gates.CRX(np.pi / 2), targets=[2], controls=[1]) qc.add_gate(T1, targets=[1]) props = qc.propagators() result1 = tensor(identity(2), customer_gate1(np.pi / 2)) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 2437cde13..7b62eafe2 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,7 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ParametricGate +from qutip_qip.operations import AngleParametricGate from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity @@ -80,7 +80,7 @@ def two_qubit_gate_compiler(self, circuit_instruction, args): ) ] - class U1(ParametricGate): + class U1(AngleParametricGate): num_qubits = 1 num_params = 1 self_inverse = False @@ -88,10 +88,7 @@ class U1(ParametricGate): def get_qobj(self): pass - def validate_params(self, arg_value): - pass - - class U2(ParametricGate): + class U2(AngleParametricGate): num_qubits = 2 num_params = 1 self_inverse = False @@ -99,12 +96,9 @@ class U2(ParametricGate): def get_qobj(self): pass - def validate_params(self, arg_value): - pass - num_qubits = 2 circuit = QubitCircuit(num_qubits) - circuit.add_gate(U1(arg_value=1.0), targets=0) + circuit.add_gate(U1(1.0), targets=0) circuit.add_gate(U2(1.0), targets=[0, 1]) circuit.add_gate(U1(1.0), targets=0) diff --git a/tests/test_device.py b/tests/test_device.py index 01b43d9eb..cbdbdf874 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -34,9 +34,9 @@ _tol = 3.0e-2 num_qubits = 2 -_rx = RX(arg_value=np.pi / 2, arg_label=r"\pi/2") -_ry = RY(arg_value=np.pi / 2, arg_label=r"\pi/2") -_rz = RZ(arg_value=np.pi / 2, arg_label=r"\pi/2") +_rx = RX(np.pi / 2, arg_label=r"\pi/2") +_ry = RY(np.pi / 2, arg_label=r"\pi/2") +_rz = RZ(np.pi / 2, arg_label=r"\pi/2") single_gate_tests = [ @@ -127,7 +127,7 @@ def test_numerical_evolution(num_qubits, gates, targets, device_class, kwargs): # Test for RZX gate, only available on SCQubits. -_rzx = RZX(arg_value=np.pi / 2) +_rzx = RZX(np.pi / 2) @pytest.mark.parametrize( diff --git a/tests/test_gates.py b/tests/test_gates.py index eab8b9cbd..2376b043e 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -121,7 +121,7 @@ def test_toffoli(self, permutation): ) def test_molmer_sorensen(self, angle, expected): np.testing.assert_allclose( - gates.MS(angle).get_qobj().full(), expected.full(), atol=1e-15 + gates.MS(*angle).get_qobj().full(), expected.full(), atol=1e-15 ) @pytest.mark.parametrize( @@ -136,7 +136,7 @@ def test_molmer_sorensen(self, angle, expected): ) def test_zero_rotations_are_identity(self, gate, n_angles): np.testing.assert_allclose( - np.eye(2), gate([0] * n_angles).get_qobj().full(), atol=1e-15 + np.eye(2), gate(*[0] * n_angles).get_qobj().full(), atol=1e-15 ) @@ -146,7 +146,8 @@ class TestCliffordGroup: group for a single qubit. """ - clifford = list(qubit_clifford_group()) + with pytest.warns(DeprecationWarning): + clifford = list(qubit_clifford_group()) pauli = [qutip.qeye(2), qutip.sigmax(), qutip.sigmay(), qutip.sigmaz()] def test_single_qubit_group_dimension_is_24(self): @@ -208,7 +209,8 @@ class TestGateExpansion: def test_single_qubit_rotation(self, gate: Gate, n_angles: int): base = qutip.rand_ket(2) if n_angles > 0: - gate = gate(2 * np.pi * np.random.rand(n_angles)) + angles = 2 * np.pi * (np.random.rand(n_angles)) + gate = gate(*angles) applied = gate.get_qobj() * base random = [qutip.rand_ket(2) for _ in [None] * (self.n_qubits - 1)] @@ -237,9 +239,7 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): pytest.param(gates.SWAP, 0, id="SWAP"), pytest.param(gates.ISWAP, 0, id="ISWAP"), pytest.param(gates.SQRTSWAP, 0, id="SQRTSWAP"), - pytest.param( - gates.MS([0.5 * np.pi, 0.0]), 0, id="Molmer-Sorensen" - ), + pytest.param(gates.MS(0.5 * np.pi, 0.0), 0, id="Molmer-Sorensen"), ], ) def test_two_qubit(self, gate, n_controls): @@ -409,21 +409,17 @@ def test_gates_class(): circuit1.add_gate(gates.SQRTX, targets=0) circuit1.add_gate(gates.S, targets=2) circuit1.add_gate(gates.T, targets=1) - circuit1.add_gate(gates.R(arg_value=(np.pi / 4, np.pi / 6)), targets=1) - circuit1.add_gate( - gates.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0 - ) + circuit1.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit1.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) circuit1.add_gate(gates.CX, controls=0, targets=1) - circuit1.add_gate(gates.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) + circuit1.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) circuit1.add_gate(gates.SWAP, targets=[0, 1]) circuit1.add_gate(gates.ISWAP, targets=[2, 1]) circuit1.add_gate(gates.CZ, controls=[0], targets=[2]) circuit1.add_gate(gates.SQRTSWAP, [2, 0]) circuit1.add_gate(gates.SQRTISWAP, [0, 1]) circuit1.add_gate(gates.SWAPALPHA(np.pi / 4), [1, 2]) - circuit1.add_gate( - gates.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0] - ) + circuit1.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) circuit1.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) circuit1.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) circuit1.add_gate(gates.BERKELEY, targets=[1, 0]) @@ -441,26 +437,21 @@ def test_gates_class(): circuit2.add_gate(gates.SQRTX, targets=0) circuit2.add_gate(gates.S, targets=2) circuit2.add_gate(gates.T, targets=1) - circuit2.add_gate(gates.R([np.pi / 4, np.pi / 6]), targets=1) - circuit2.add_gate( - gates.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), - targets=0, - ) + circuit2.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit2.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) circuit2.add_gate(gates.CX, controls=0, targets=1) - circuit2.add_gate(gates.CPHASE(arg_value=np.pi / 4), controls=0, targets=1) + circuit2.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) circuit2.add_gate(gates.SWAP, targets=[0, 1]) circuit2.add_gate(gates.ISWAP, targets=[2, 1]) circuit2.add_gate(gates.CZ, controls=[0], targets=[2]) circuit2.add_gate(gates.SQRTSWAP, targets=[2, 0]) circuit2.add_gate(gates.SQRTISWAP, targets=[0, 1]) circuit2.add_gate(gates.SWAPALPHA(np.pi / 4), targets=[1, 2]) - circuit2.add_gate( - gates.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0] - ) + circuit2.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) circuit2.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) circuit2.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) circuit2.add_gate(gates.BERKELEY, targets=[1, 0]) - circuit2.add_gate(gates.RZX(arg_value=1.0), targets=[1, 0]) + circuit2.add_gate(gates.RZX(1.0), targets=[1, 0]) result2 = circuit2.run(init_state) assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 @@ -491,10 +482,10 @@ def test_gates_class(): gates.RY(0.5), gates.RZ(0.5), gates.PHASE(0.5), - gates.R((0.5, 0.9)), - gates.QASMU((0.1, 0.2, 0.3)), + gates.R(0.5, 0.9), + gates.QASMU(0.1, 0.2, 0.3), gates.SWAPALPHA(0.3), - gates.MS((0.47, 0.8)), + gates.MS(0.47, 0.8), gates.RZX(0.6), ] @@ -505,11 +496,11 @@ def test_gates_class(): gates.CH, gates.CS, gates.CT, - gates.CRX(arg_value=0.7), - gates.CRY(arg_value=0.88), - gates.CRZ(arg_value=0.78), - gates.CPHASE(arg_value=0.9), - gates.CQASMU(arg_value=[0.9, 0.22, 0.15]), + gates.CRX(0.7), + gates.CRY(0.88), + gates.CRZ(0.78), + gates.CPHASE(0.9), + gates.CQASMU(0.9, 0.22, 0.15), gates.TOFFOLI, gates.FREDKIN, ] diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 7f7823527..135fd0ac9 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -137,16 +137,16 @@ def test_qasm_str(): def test_export_import(): qc = QubitCircuit(3) - qc.add_gate(gates.CRY(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(gates.CRX(arg_value=np.pi), targets=1, controls=0) - qc.add_gate(gates.CRZ(arg_value=np.pi), targets=1, controls=0) + qc.add_gate(gates.CRY(np.pi), targets=1, controls=0) + qc.add_gate(gates.CRX(np.pi), targets=1, controls=0) + qc.add_gate(gates.CRZ(np.pi), targets=1, controls=0) qc.add_gate(gates.CX, targets=1, controls=0) qc.add_gate(gates.TOFFOLI, targets=2, controls=[0, 1]) # qc.add_gate(SQRTX, targets=0) qc.add_gate(gates.CS, targets=1, controls=0) qc.add_gate(gates.CT, targets=1, controls=0) qc.add_gate(gates.SWAP, targets=[0, 1]) - qc.add_gate(gates.QASMU(arg_value=[np.pi, np.pi, np.pi]), targets=[0]) + qc.add_gate(gates.QASMU(np.pi, np.pi, np.pi), targets=[0]) qc.add_gate(gates.RX(np.pi), targets=[0]) qc.add_gate(gates.RY(np.pi), targets=[0]) qc.add_gate(gates.RZ(np.pi), targets=[0]) @@ -156,7 +156,7 @@ def test_export_import(): qc.add_gate(gates.Z, targets=[0]) qc.add_gate(gates.S, targets=[0]) qc.add_gate(gates.T, targets=[0]) - # qc.add_gate(CZ, targets=[0], controls=[1]) + # qc.add_gate(gates.CZ, targets=[0], controls=[1]) # The generated code by default has a inclusion statement of # qelib1.inc, which will trigger a warning when read. diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5b0c82dc3..737643582 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -23,13 +23,13 @@ def qc1(): qc = QubitCircuit(4) qc.add_gate(ISWAP, targets=[2, 3]) - qc.add_gate(CRX(arg_value=np.pi / 2), targets=[0], controls=[1]) + qc.add_gate(CRX(np.pi / 2), targets=[0], controls=[1]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(TOFFOLI, controls=[0, 2], targets=[1]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(arg_value=0.5), controls=[2], targets=[3]) + qc.add_gate(CRX(0.5), controls=[2], targets=[3]) qc.add_gate(SWAP, targets=[0, 3]) return qc @@ -71,7 +71,7 @@ def qc2(): qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(arg_value=0.5), controls=[0], targets=[1]) + qc.add_gate(CRX(0.5), controls=[0], targets=[1]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(SWAP, targets=[0, 3]) qc.add_measurement("M", targets=[0], classical_store=0) @@ -152,11 +152,11 @@ def qc3(): qc.add_gate(BERKELEY, targets=[0, 3]) qc.add_gate(FREDKIN, controls=[3], targets=[1, 2]) qc.add_gate(CX, controls=[0], targets=[1]) - qc.add_gate(CRX(arg_value=0.5), controls=[0], targets=[1]) + qc.add_gate(CRX(0.5), controls=[0], targets=[1]) qc.add_measurement("M", targets=[0], classical_store=0) qc.add_gate(SWAP, targets=[0, 3]) qc.add_gate(TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(CPHASE(arg_value=0.75), controls=[2], targets=[3]) + qc.add_gate(CPHASE(0.75), controls=[2], targets=[3]) qc.add_gate(ISWAP, targets=[1, 3]) qc.add_measurement("M", targets=[1], classical_store=1) return qc From ca8734cc7995dd33d30ac6aa066a3bad7031361c Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 01:00:28 +0530 Subject: [PATCH 059/117] Added a utils.py for helper functions --- .../decompose/decompose_single_qubit_gate.py | 10 +++++----- src/qutip_qip/{decompose/_utility.py => utils.py} | 11 +++++++++-- .../test_utility.py => test_utils.py} | 15 ++++++--------- 3 files changed, 20 insertions(+), 16 deletions(-) rename src/qutip_qip/{decompose/_utility.py => utils.py} (79%) rename tests/{decomposition_functions/test_utility.py => test_utils.py} (82%) diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index ab653dcf5..7154ff62a 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -2,7 +2,7 @@ import numpy as np import cmath -from qutip_qip.decompose._utility import check_gate +from qutip_qip.utils import valid_unitary from qutip_qip.operations.gates import GLOBALPHASE, X, RX, RY, RZ @@ -55,7 +55,7 @@ def _ZYZ_rotation(input_gate): input_gate : :class:`qutip.Qobj` The matrix that's supposed to be decomposed should be a Qobj. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) Phase_gate = GLOBALPHASE(global_phase_angle) @@ -84,7 +84,7 @@ def _ZXZ_rotation(input_gate): input_gate : :class:`qutip.Qobj` The matrix that's supposed to be decomposed should be a Qobj. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) alpha = alpha - np.pi / 2 beta = beta + np.pi / 2 @@ -113,7 +113,7 @@ def _ZXZ_rotation(input_gate): def _ZYZ_pauli_X(input_gate): """Returns a 1 qubit unitary as a product of ZYZ rotation matrices and Pauli X.""" - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) Phase_gate = GLOBALPHASE(global_phase_angle) @@ -221,7 +221,7 @@ def decompose_one_qubit_gate(input_gate, method): :math:`\textrm{B}`, 1 gates forming :math:`\textrm{C}`, and some global phase gate. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) f = _single_decompositions_dictionary.get(method, None) if f is None: raise MethodError(f"Invalid decomposition method: {method!r}") diff --git a/src/qutip_qip/decompose/_utility.py b/src/qutip_qip/utils.py similarity index 79% rename from src/qutip_qip/decompose/_utility.py rename to src/qutip_qip/utils.py index 48553b207..8ab25eb05 100644 --- a/src/qutip_qip/decompose/_utility.py +++ b/src/qutip_qip/utils.py @@ -1,8 +1,13 @@ +""" +Module for Helper functions. +""" + from qutip import Qobj +__all__ = ['valid_unitary'] -def check_gate(gate, num_qubits): - """Verifies input is a valid quantum gate. +def valid_unitary(gate, num_qubits): + """Verifies input is a valid quantum gate i.e. unitary Qobj. Parameters ---------- @@ -19,7 +24,9 @@ def check_gate(gate, num_qubits): """ if not isinstance(gate, Qobj): raise TypeError("The input matrix is not a Qobj.") + if not gate.isunitary: raise ValueError("Input is not unitary.") + if gate.dims != [[2] * num_qubits] * 2: raise ValueError(f"Input is not a unitary on {num_qubits} qubits.") diff --git a/tests/decomposition_functions/test_utility.py b/tests/test_utils.py similarity index 82% rename from tests/decomposition_functions/test_utility.py rename to tests/test_utils.py index 3c392782a..5aba04d64 100644 --- a/tests/decomposition_functions/test_utility.py +++ b/tests/test_utils.py @@ -1,13 +1,10 @@ import numpy as np import pytest - from qutip import Qobj, qeye -from qutip_qip.decompose._utility import ( - check_gate, -) +from qutip_qip.utils import valid_unitary -# Tests for check_gate +# Tests for valid_unitary @pytest.mark.parametrize( "invalid_input", [ @@ -25,14 +22,14 @@ def test_check_gate_non_qobj(invalid_input): """Checks if correct value is returned or not when the input is not a Qobj .""" with pytest.raises(TypeError, match="The input matrix is not a Qobj."): - check_gate(invalid_input, num_qubits=1) + valid_unitary(invalid_input, num_qubits=1) @pytest.mark.parametrize("non_unitary", [Qobj([[1, 1], [0, 1]])]) def test_check_gate_non_unitary(non_unitary): """Checks if non-unitary input is correctly identified.""" with pytest.raises(ValueError, match="Input is not unitary."): - check_gate(non_unitary, num_qubits=1) + valid_unitary(non_unitary, num_qubits=1) @pytest.mark.parametrize("non_1qubit_unitary", [qeye(4)]) @@ -42,11 +39,11 @@ def test_check_gate_non_1qubit(non_1qubit_unitary): with pytest.raises( ValueError, match=f"Input is not a unitary on {num_qubits} qubits." ): - check_gate(non_1qubit_unitary, num_qubits) + valid_unitary(non_1qubit_unitary, num_qubits) @pytest.mark.parametrize("unitary", [Qobj([[1, 0], [0, -1]])]) def test_check_gate_unitary_input(unitary): """Checks if shape of input is correctly identified.""" # No error raised if it passes. - check_gate(unitary, num_qubits=1) + valid_unitary(unitary, num_qubits=1) From 0d7af5dd4532f6dd9bb29d70fe81d9c1364cec51 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 02:10:03 +0530 Subject: [PATCH 060/117] Corrected several isInstance in the codebase --- src/qutip_qip/algorithms/qpe.py | 2 +- src/qutip_qip/circuit/circuit.py | 2 +- src/qutip_qip/circuit/draw/base_renderer.py | 2 +- src/qutip_qip/circuit/instruction.py | 2 +- src/qutip_qip/device/model.py | 2 +- src/qutip_qip/device/processor.py | 18 ++++++++---- src/qutip_qip/device/utils.py | 10 ++++--- src/qutip_qip/operations/utils.py | 2 +- src/qutip_qip/pulse/evo_element.py | 2 +- src/qutip_qip/qiskit/backend.py | 3 +- src/qutip_qip/qiskit/utils/converter.py | 3 +- src/qutip_qip/vqa.py | 31 ++++++++++++++++----- tests/test_compiler.py | 4 +-- tests/test_device.py | 6 ++++ tests/test_model.py | 2 ++ 15 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 377bfdb79..6b4ae0af2 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -42,7 +42,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): target_qubits = list( range(num_counting_qubits, num_counting_qubits + num_target_qubits) ) - elif isinstance(target_qubits, int): + elif type(target_qubits) is int: target_qubits = [target_qubits] num_target_qubits = 1 else: diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index e2f5c0b3a..922704bfb 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -629,7 +629,7 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): basis_1q = [] basis_2q = [] - if isinstance(basis, list): + if isinstance(basis, Iterable): for gate in basis: if gate in basis_2q_valid: basis_2q.append(gate) diff --git a/src/qutip_qip/circuit/draw/base_renderer.py b/src/qutip_qip/circuit/draw/base_renderer.py index 88649c799..deefb886c 100644 --- a/src/qutip_qip/circuit/draw/base_renderer.py +++ b/src/qutip_qip/circuit/draw/base_renderer.py @@ -92,7 +92,7 @@ class StyleConfig: wire_color: str | None = None def __post_init__(self): - if isinstance(self.bulge, bool): + if type(self.bulge) is bool: self.bulge = "round4" if self.bulge else "square" if self.theme == "qutip": diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index 4ae2047cf..2a40e7dee 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -8,7 +8,7 @@ def _validate_non_negative_int_tuple(T: any, txt: str = ""): raise TypeError(f"Must pass a tuple for {txt}, got {type(T)}") for q in T: - if not isinstance(q, int): + if type(q) is not int: raise ValueError(f"All {txt} indices must be an int, found {q}") if q < 0: diff --git a/src/qutip_qip/device/model.py b/src/qutip_qip/device/model.py index a8e35dcf2..408a3c50e 100644 --- a/src/qutip_qip/device/model.py +++ b/src/qutip_qip/device/model.py @@ -67,7 +67,7 @@ def get_control(self, label: Hashable) -> Tuple[Qobj, List[int]]: The control Hamiltonian in the form of ``(qobj, targets)``. """ if hasattr(self, "_old_index_label_map"): - if isinstance(label, int): + if type(label) is int: label = self._old_index_label_map[label] return self._controls[label] diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 6e12dcc9b..66b952b5d 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -1,4 +1,4 @@ -from collections.abc import Iterable +from collections.abc import Sequence import warnings from copy import deepcopy import numpy as np @@ -176,7 +176,7 @@ def _get_drift_obj(self): def _unify_targets(self, qobj, targets): if targets is None: targets = list(range(len(qobj.dims[0]))) - if not isinstance(targets, Iterable): + if not isinstance(targets, Sequence): targets = [targets] return targets @@ -380,7 +380,7 @@ def coeffs(self, coeffs): self.set_coeffs(coeffs) def _generate_iterator_from_dict_or_list(self, value): - if isinstance(value, dict): + if type(value) is dict: iterator = value.items() elif isinstance(value, (list, np.ndarray)): iterator = enumerate(value) @@ -442,6 +442,7 @@ def set_tlist(self, tlist): for pulse in self.pulses: pulse.tlist = tlist return + iterator = self._generate_iterator_from_dict_or_list(tlist) pulse_dict = self.get_pulse_dict() for pulse_label, value in iterator: @@ -490,24 +491,29 @@ def get_full_coeffs(self, full_tlist=None): self._is_pulses_valid() if not self.pulses: return np.array((0, 0), dtype=float) + if full_tlist is None: full_tlist = self.get_full_tlist() + coeffs_list = [] for pulse in self.pulses: if pulse.tlist is None and pulse.coeff is None: coeffs_list.append(np.zeros(len(full_tlist))) continue + if not isinstance(pulse.coeff, (bool, np.ndarray)): raise ValueError( "get_full_coeffs only works for " "NumPy array or bool coeff." ) - if isinstance(pulse.coeff, bool): + + if type(pulse.coeff) is bool: if pulse.coeff: coeffs_list.append(np.ones(len(full_tlist))) else: coeffs_list.append(np.zeros(len(full_tlist))) continue + if self.spline_kind == "step_func": arg = {"_step_func_coeff": True} coeffs_list.append( @@ -623,7 +629,7 @@ def remove_pulse(self, indices=None, label=None): The label of the pulse """ if indices is not None: - if not isinstance(indices, Iterable): + if not isinstance(indices, Sequence): indices = [indices] indices.sort(reverse=True) for ind in indices: @@ -690,7 +696,7 @@ def get_pulse_dict(self): def find_pulse(self, pulse_name): pulse_dict = self.get_pulse_dict() - if isinstance(pulse_name, int): + if type(pulse_name) is int: return self.pulses[pulse_name] else: try: diff --git a/src/qutip_qip/device/utils.py b/src/qutip_qip/device/utils.py index afea75469..1e363d5a6 100644 --- a/src/qutip_qip/device/utils.py +++ b/src/qutip_qip/device/utils.py @@ -20,22 +20,24 @@ def _pulse_interpolate(pulse, tlist): if pulse.tlist is None and pulse.coeff is None: coeff = np.zeros(len(tlist)) return coeff + if isinstance(pulse.coeff, bool): if pulse.coeff: coeff = np.ones(len(tlist)) else: coeff = np.zeros(len(tlist)) return coeff + coeff = pulse.coeff if len(coeff) == len(pulse.tlist) - 1: # for discrete pulse coeff = np.concatenate([coeff, [0]]) - from scipy import interpolate - + kind = "cubic" if pulse.spline_kind == "step_func": kind = "previous" - else: - kind = "cubic" + + from scipy import interpolate + inter = interpolate.interp1d( pulse.tlist, coeff, kind=kind, bounds_error=False, fill_value=0.0 ) diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 4e65adaf6..c8cbf89d7 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -65,7 +65,7 @@ def _targets_to_list( # if targets is a list of integer if targets is None: targets = list(range(len(oper.dims[0]))) - if not hasattr(targets, "__iter__"): + if not isinstance(targets, Iterable): targets = [targets] if not all([isinstance(t, numbers.Integral) for t in targets]): raise TypeError("targets should be an integer or a list of integer") diff --git a/src/qutip_qip/pulse/evo_element.py b/src/qutip_qip/pulse/evo_element.py index f81f2414a..395d04f50 100644 --- a/src/qutip_qip/pulse/evo_element.py +++ b/src/qutip_qip/pulse/evo_element.py @@ -68,7 +68,7 @@ def _get_qobjevo_helper( if self.tlist is None and self.coeff is None: qu = QobjEvo(mat) * 0.0 - elif isinstance(self.coeff, bool): + elif type(self.coeff) is bool: if self.coeff: if self.tlist is None: qu = QobjEvo(mat, tlist=self.tlist) diff --git a/src/qutip_qip/qiskit/backend.py b/src/qutip_qip/qiskit/backend.py index f509a6973..fe52783f2 100644 --- a/src/qutip_qip/qiskit/backend.py +++ b/src/qutip_qip/qiskit/backend.py @@ -1,6 +1,7 @@ """Backends for simulating qiskit circuits.""" from abc import abstractmethod +from collections.abc import Sequence import uuid from qiskit.circuit import QuantumCircuit @@ -176,7 +177,7 @@ def run( Job object that stores results and execution data. """ - if not isinstance(run_input, list): + if not isinstance(run_input, Sequence): run_input = [run_input] for circuit in run_input: diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index 64204da53..1a707f4e4 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -1,5 +1,6 @@ """Conversion of circuits from qiskit to qutip_qip.""" +from collections.abc import Iterable from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import Gate @@ -57,7 +58,7 @@ def get_qutip_index(bit_index: int | list, total_bits: int) -> int: the 0st bit is mapped to the 0th bit and (n-1)th bit to (n-q)th bit and so on. Essentially the bit order stays the same. """ - if isinstance(bit_index, list): + if isinstance(bit_index, Iterable): return [get_qutip_index(bit, total_bits) for bit in bit_index] else: return bit_index diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index cb8232bdc..6fb250dd7 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -2,11 +2,14 @@ import types import random +from collections.abc import Sequence + import numpy as np from qutip import basis, tensor, Qobj, qeye, expect -from qutip_qip.circuit import QubitCircuit from scipy.optimize import minimize from scipy.linalg import expm_frechet + +from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import gate_sequence_product, unitary_gate, Gate @@ -49,12 +52,16 @@ def __init__(self, num_qubits, num_layers=1, cost_method="OBSERVABLE"): if self.num_qubits < 1: raise ValueError("Expected 1 or more qubits") - if not isinstance(self.num_qubits, int): + + if type(self.num_qubits) is not int: raise TypeError("Expected an integer number of qubits") + if self.num_layers < 1: raise ValueError("Expected 1 or more layer") - if not isinstance(self.num_layers, int): + + if type(self.num_layers) is not int: raise TypeError("Expected an integer number of layers") + if self.cost_method not in self._cost_methods: raise ValueError( f"Cost method {self.cost_method} not one of " @@ -293,20 +300,22 @@ def optimize_parameters( n_free_params = self.get_free_parameters_num() # Set initial circuit parameters - if isinstance(initial, str): + if type(initial) is str: if initial == "random": angles = [random.random() for i in range(n_free_params)] elif initial == "ones": angles = [1 for i in range(n_free_params)] else: raise ValueError("Invalid initial condition string") - elif isinstance(initial, list) or isinstance(initial, np.ndarray): + + elif isinstance(initial, Sequence): if len(initial) != n_free_params: raise ValueError( f"Expected {n_free_params} initial parameters" f"but got {len(initial)}." ) angles = initial + else: raise ValueError( "Initial conditions were neither a list of values" @@ -567,14 +576,18 @@ def __init__( if isinstance(operator, Qobj): if not self.is_unitary: self.num_parameters = 1 - elif isinstance(operator, str): + + elif type(operator) is str: self.is_native_gate = True if targets is None: raise ValueError("Targets must be specified for native gates") + elif isinstance(operator, ParameterizedHamiltonian): self.num_parameters = operator.num_parameters + elif isinstance(operator, types.FunctionType): self.num_parameters = 1 + else: raise ValueError( "operator should be either: Qobj | function which" @@ -612,13 +625,15 @@ def get_unitary(self, angles=None): # Case where the operator is a string referring to an existing gate. if self.is_native_gate: raise TypeError("Can't compute unitary of native gate") + # Function returning Qobj unitary if isinstance(self.operator, types.FunctionType): - # In the future, this could be generalized to multiple angles + # TODO In the future, this could be generalized to multiple angles unitary = self.operator(angles[0]) if not isinstance(unitary, Qobj): raise TypeError("Provided function does not return Qobj") return unitary + # ParameterizedHamiltonian instance if isinstance(self.operator, ParameterizedHamiltonian): return (-1j * self.operator.get_hamiltonian(angles)).expm() @@ -656,6 +671,7 @@ def get_unitary_derivative(self, angles, term_index=0): "Can only take derivative of block specified " "by Hamiltonians or ParameterizedHamiltonian instances." ) + if isinstance(self.operator, ParameterizedHamiltonian): arg = -1j * self.operator.get_hamiltonian(angles) direction = -1j * self.operator.p_terms[term_index] @@ -663,6 +679,7 @@ def get_unitary_derivative(self, angles, term_index=0): expm_frechet(arg.full(), direction.full(), compute_expm=False), dims=direction.dims, ) + if len(angles) != 1: raise ValueError( "Expected a single angle for non-" diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 7b62eafe2..8240db012 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -231,9 +231,9 @@ def test_compiler_result_format(): compiler.gate_compiler["RX"] = rx_compiler_without_pulse_dict tlist, coeffs = compiler.compile(circuit) - assert isinstance(tlist, dict) + assert type(tlist) is dict assert 0 in tlist - assert isinstance(coeffs, dict) + assert type(coeffs) is dict assert 0 in coeffs processor.coeffs = coeffs processor.set_all_tlist(tlist) diff --git a/tests/test_device.py b/tests/test_device.py index cbdbdf874..dc6ba687a 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -155,21 +155,25 @@ def _test_numerical_evolution_helper( state = qutip.rand_ket(2**num_qubits) state.dims = [[2] * num_qubits, [1] * num_qubits] target = circuit.run(state) + if isinstance(device, DispersiveCavityQED): num_ancilla = len(device.dims) - num_qubits ancilla_indices = slice(0, num_ancilla) extra = qutip.basis(device.dims[ancilla_indices], [0] * num_ancilla) init_state = qutip.tensor(extra, state) + elif isinstance(device, SCQubits): # expand to 3-level represetnation init_state = _ket_expaned_dims(state, device.dims) else: init_state = state + options = {"store_final_state": True, "nsteps": 50000} result = device.run_state( init_state=init_state, analytical=False, options=options ) numerical_result = result.final_state + if isinstance(device, DispersiveCavityQED): target = qutip.tensor(extra, target) elif isinstance(device, SCQubits): @@ -230,10 +234,12 @@ def test_numerical_circuit(circuit, device_class, kwargs, schedule_mode): init_state = _ket_expaned_dims(state, device.dims) else: init_state = state + options = {"store_final_state": True, "nsteps": 50000} result = device.run_state( init_state=init_state, analytical=False, options=options ) + if isinstance(device, DispersiveCavityQED): target = qutip.tensor(extra, target) elif isinstance(device, SCQubits): diff --git a/tests/test_model.py b/tests/test_model.py index fc1efdb09..b9443cc4e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -30,11 +30,13 @@ def test_cavityqed_model(): def test_spinchain_model(model_class): model = LinearSpinChain(3, sx=[1.1, 1, 0, 0.8], sz=7.0, t1=10.0) assert model.get_all_drift() == [] + model.get_control_labels() if isinstance(model, LinearSpinChain): assert len(model.get_control_labels()) == 3 * 3 - 1 elif isinstance(model, CircularSpinChain): assert len(model.get_control_labels()) == 3 * 3 + model.get_control("g1") model.get_control("sx0") assert_array_equal(model.params["sz"], 7.0) From 5026db48fa398dc7afbda0f11dfdfcfca05a0ea1 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 02:57:53 +0530 Subject: [PATCH 061/117] Add setter method for arg_value My validation method need not act on my arguments individually, instead it can act on the combined space. This is why the setter must take in all the argument values and assign them. Second frontend interface __init__ for Controlled Parametrised Gates like CRX etc. were added --- src/qutip_qip/algorithms/qft.py | 4 +- src/qutip_qip/circuit/_decompose.py | 4 +- src/qutip_qip/circuit/circuit.py | 6 +- .../circuit/simulator/matrix_mul_simulator.py | 4 +- src/qutip_qip/compiler/cavityqedcompiler.py | 8 +- .../decompose/decompose_single_qubit_gate.py | 2 +- src/qutip_qip/operations/controlled.py | 2 +- src/qutip_qip/operations/gateclass.py | 15 +- src/qutip_qip/operations/gates/other_gates.py | 11 +- .../operations/gates/single_qubit_gate.py | 51 ++++--- .../operations/gates/two_qubit_gate.py | 137 +++++++++++------- src/qutip_qip/operations/parametric.py | 15 +- src/qutip_qip/utils.py | 3 +- tests/test_circuit.py | 12 +- 14 files changed, 159 insertions(+), 115 deletions(-) diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index e1df57a3f..a84a1a645 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -140,6 +140,6 @@ def _cphase_to_cnot(targets, controls, arg_value, qc: QubitCircuit): qc.add_gate(decomposed_gates[4], targets=targets) qc.add_gate(CX, targets=targets, controls=controls) qc.add_gate(RZ(arg_value / 2), targets=controls) - gate = decomposed_gates[7] # This is a GLOBALPHASE Gate - gate.phase += arg_value / 4 + gate = decomposed_gates[7] # This is a GLOBALPHASE Gate + gate.arg_value = gate.arg_value[0] + arg_value / 4 qc.add_gate(gate, targets=targets) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 2c0daf404..23cc1a96d 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -227,9 +227,7 @@ def _gate_FREDKIN(circ_instruction, temp_resolved): gate=RZ(-3 * pi / 4, arg_label=r"-3\pi/4"), targets=targets[1], ) - temp_resolved.add_gate( - RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1] - ) + temp_resolved.add_gate(RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1]) temp_resolved.add_gate( RZ(-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 922704bfb..8c8d6280c 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -720,7 +720,7 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets=targets, ) qc_temp.add_gate( - RZ(gate.theta, arg_label=gate.arg_label), + RZ(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( @@ -734,7 +734,7 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets=targets, ) qc_temp.add_gate( - RX(gate.theta, arg_label=gate.arg_label), + RX(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( @@ -748,7 +748,7 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets=targets, ) qc_temp.add_gate( - RY(gate.theta, arg_label=gate.arg_label), + RY(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( diff --git a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py index e07c61ec9..3620fe4f4 100644 --- a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py +++ b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py @@ -256,7 +256,9 @@ def step(self): state = self._evolve_state(gate, qubits, current_state) else: - raise ValueError(f"Invalid operation {self.qc.instructions[self._op_index]}") + raise ValueError( + f"Invalid operation {self.qc.instructions[self._op_index]}" + ) self._state = state self._op_index += 1 diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 98f28c5f8..5a97f1785 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -194,17 +194,13 @@ def _swap_compiler( # corrections compiled_gate1 = self.gate_compiler["RZ"]( - GateInstruction( - operation=RZ(correction_angle), qubits=(q1,) - ), + GateInstruction(operation=RZ(correction_angle), qubits=(q1,)), args, ) instruction_list += compiled_gate1 compiled_gate2 = self.gate_compiler["RZ"]( - GateInstruction( - operation=RZ(correction_angle), qubits=(q2,) - ), + GateInstruction(operation=RZ(correction_angle), qubits=(q2,)), args, ) instruction_list += compiled_gate2 diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index 7154ff62a..18b454277 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -118,7 +118,7 @@ def _ZYZ_pauli_X(input_gate): Phase_gate = GLOBALPHASE(global_phase_angle) Rz_A = RZ( -alpha, + alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) Ry_A = RY( diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 338cbc8d2..2ed6b204c 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -22,7 +22,7 @@ def __get__(self, instance, owner): return partial(self.func, owner) # Called on the instance (e.g., CRX(0.5).get_qobj()) - return partial(self.func, instance) + return partial(self.func, instance) class ControlledGate(Gate): diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 28d78b171..f4eb203f7 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -97,25 +97,24 @@ def __eq__(cls, other: any) -> bool: if isinstance(other, _GateMetaClass): cls_name = getattr(cls, "name", None) other_name = getattr(other, "name", None) - + cls_namespace = getattr(cls, "_namespace", "std") other_namespace = getattr(other, "_namespace", "std") - + # They are equal if they share the same name and namespace return cls_name == other_name and cls_namespace == other_namespace - + return False def __hash__(cls) -> int: """ - Required because __eq__ is overridden. + Required because __eq__ is overridden. Hashes the class based on its unique identity (namespace and name) so it can still be safely used in the _registry sets and dicts. """ - return hash(( - getattr(cls, "namespace", "std"), - getattr(cls, "name", None) - )) + return hash( + (getattr(cls, "namespace", "std"), getattr(cls, "name", None)) + ) def clear_cache(cls, namespace: str): """ diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 84ea955c2..ff2ca01f0 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -17,7 +17,8 @@ class GLOBALPHASE(AngleParametricGate): -------- >>> from qutip_qip.operations.gates import GLOBALPHASE """ - __slots__ = ("phase") + + __slots__ = "phase" num_qubits: Final[int] = 0 num_params: Final[int] = 1 @@ -26,18 +27,18 @@ class GLOBALPHASE(AngleParametricGate): def __init__(self, phase: float = 0.0): super().__init__(phase) - self.phase = phase def __repr__(self): - return f"Gate({self.name}, phase {self.phase})" + return f"Gate({self.name}, phase {self.arg_value[0]})" def get_qobj(self, num_qubits=None): + phase = self.arg_value[0] if num_qubits is None: - return Qobj(self.phase) + return Qobj(phase) N = 2**num_qubits return Qobj( - np.exp(1.0j * self.phase) + np.exp(1.0j * phase) * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], ) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 1a10eb62a..fc93abed3 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -26,7 +26,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: pass def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class X(_SingleQubitGate): @@ -376,14 +376,13 @@ class RX(_SingleQubitParametricGate): [0. -0.70711j 0.70711+0.j ]] """ - __slots__ = ("theta") + __slots__ = () num_params: Final[int] = 1 latex_str: Final[str] = r"R_x" - def __init__(self, theta: float, arg_label = None): + def __init__(self, theta: float, arg_label=None): super().__init__(theta, arg_label=arg_label) - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -398,7 +397,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return RX(theta=-self.theta) + theta = self.arg_value[0] + return RX(-theta) class RY(_SingleQubitParametricGate): @@ -415,14 +415,13 @@ class RY(_SingleQubitParametricGate): [ 0.70711 0.70711]] """ - __slots__ = ("theta") + __slots__ = () num_params: Final[int] = 1 latex_str: Final[str] = r"R_y" def __init__(self, theta: float, arg_label: str | None = None): super().__init__(theta, arg_label=arg_label) - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -436,7 +435,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return RY(theta=-self.theta) + theta = self.arg_value[0] + return RY(-theta) class RZ(_SingleQubitParametricGate): @@ -453,14 +453,13 @@ class RZ(_SingleQubitParametricGate): [0. +0.j 0.70711+0.70711j]] """ - __slots__ = ("theta") + __slots__ = () num_params: Final[int] = 1 latex_str: Final[str] = r"R_z" def __init__(self, theta: float, arg_label: str | None = None): super().__init__(theta, arg_label=arg_label) - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -469,7 +468,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) def inverse(self) -> Gate: - return RZ(theta=-self.theta) + theta = self.arg_value[0] + return RZ(-theta) class PHASE(_SingleQubitParametricGate): @@ -481,14 +481,13 @@ class PHASE(_SingleQubitParametricGate): >>> from qutip_qip.operations.gates import PHASE """ - __slots__ = ("theta") + __slots__ = () num_params: Final[int] = 1 latex_str: Final[str] = r"PHASE" def __init__(self, theta: float, arg_label: str | None = None): super().__init__(theta, arg_label=arg_label) - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -502,7 +501,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return PHASE(theta=-self.theta) + theta = self.arg_value[0] + return PHASE(-theta) class R(_SingleQubitParametricGate): @@ -526,15 +526,13 @@ class R(_SingleQubitParametricGate): [ 0.70711 0.70711]] """ - __slots__ = ("theta", "phi") + __slots__ = () num_params: Final[int] = 2 latex_str: Final[str] = r"{\rm R}" def __init__(self, phi: float, theta: float, arg_label: str | None = None): super().__init__(phi, theta, arg_label=arg_label) - self.phi = phi - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -554,7 +552,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return R(phi=self.phi, theta=-self.theta) + phi, theta = self.arg_value + return R(phi, -theta) class QASMU(_SingleQubitParametricGate): @@ -574,16 +573,19 @@ class QASMU(_SingleQubitParametricGate): [ 0.5+0.5j -0.5+0.5j]] """ - __slots__ = ("theta", "phi", "gamma") + __slots__ = () num_params: Final[int] = 3 latex_str: Final[str] = r"{\rm QASMU}" - def __init__(self, theta: float, phi: float, gamma: float, arg_label: str | None = None): + def __init__( + self, + theta: float, + phi: float, + gamma: float, + arg_label: str | None = None, + ): super().__init__(theta, phi, gamma, arg_label=arg_label) - self.theta = theta - self.phi = phi - self.gamma = gamma @staticmethod @lru_cache(maxsize=128) @@ -603,4 +605,5 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return QASMU(-self.theta, -self.gamma, -self.phi) + theta, phi, gamma = self.arg_value + return QASMU(-theta, -gamma, -phi) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index b4faea6c7..43567c40a 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -40,7 +40,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: pass def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class _ControlledTwoQubitGate(ControlledGate): @@ -176,7 +176,7 @@ class SQRTSWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( np.array( [ @@ -217,7 +217,7 @@ class SQRTSWAPdag(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( np.array( [ @@ -259,7 +259,7 @@ class SQRTISWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( np.array( [ @@ -301,7 +301,7 @@ class SQRTISWAPdag(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( np.array( [ @@ -351,7 +351,7 @@ class BERKELEY(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [ [np.cos(np.pi / 8), 0, 0, 1.0j * np.sin(np.pi / 8)], @@ -399,7 +399,7 @@ class BERKELEYdag(_TwoQubitGate): @staticmethod @cache - def get_qobj(): + def get_qobj() -> Qobj: return Qobj( [ [np.cos(np.pi / 8), 0, 0, -1.0j * np.sin(np.pi / 8)], @@ -440,14 +440,13 @@ class SWAPALPHA(_TwoQubitParametricGate): [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] """ - __slots__ = ("alpha") + __slots__ = "alpha" num_params: Final[int] = 1 latex_str: Final[str] = r"{\rm SWAPALPHA}" def __init__(self, alpha: float, arg_label: str | None = None): super().__init__(alpha, arg_label=arg_label) - self.alpha = alpha @staticmethod @lru_cache(maxsize=128) @@ -474,7 +473,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return SWAPALPHA(alpha=-self.alpha) + alpha = self.arg_value[0] + return SWAPALPHA(-alpha) class MS(_TwoQubitParametricGate): @@ -509,8 +509,6 @@ class MS(_TwoQubitParametricGate): def __init__(self, theta: float, phi: float, arg_label: str | None = None): super().__init__(theta, phi, arg_label=arg_label) - self.theta = theta - self.phi = phi @staticmethod @lru_cache(maxsize=128) @@ -537,7 +535,8 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ) def inverse(self) -> Gate: - return MS(theta=-self.theta, phi=self.phi) + theta, phi = self.arg_value + return MS(-theta, phi) class RZX(_TwoQubitParametricGate): @@ -565,14 +564,13 @@ class RZX(_TwoQubitParametricGate): [0.+0.j 0.+0.j 0.+1.j 0.+0.j]] """ - __slots__ = ("theta") + __slots__ = "theta" num_params: Final[int] = 1 latex_str: Final[str] = r"{\rm RZX}" def __init__(self, theta: float, arg_label: str | None = None): super().__init__(theta, arg_label=arg_label) - self.theta = theta @staticmethod @lru_cache(maxsize=128) @@ -592,7 +590,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: def inverse(self) -> Gate: theta = self.arg_value[0] - return RZX(theta=-theta) + return RZX(-theta) class CX(_ControlledTwoQubitGate): @@ -838,21 +836,25 @@ class CRX(_ControlledTwoQubitGate): target_gate: Final[Type[Gate]] = RX latex_str: Final[str] = r"{\rm CRX}" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: - phi = arg_value[0] + theta = arg_value[0] return Qobj( - [[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, np.cos(phi / 2), -1j * np.sin(phi / 2)], - [0, 0, -1j * np.sin(phi / 2), np.cos(phi / 2)] + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(theta / 2), -1j * np.sin(theta / 2)], + [0, 0, -1j * np.sin(theta / 2), np.cos(theta / 2)], ], - dims = [[2,2], [2,2]] + dims=[[2, 2], [2, 2]], ) def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class CRY(_ControlledTwoQubitGate): @@ -870,21 +872,25 @@ class CRY(_ControlledTwoQubitGate): target_gate: Final[Type[Gate]] = RY latex_str: Final[str] = r"{\rm CRY}" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: - phi = arg_value[0] + theta = arg_value[0] return Qobj( - [[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, np.cos(phi / 2), -np.sin(phi / 2)], - [0, 0, np.sin(phi / 2), np.cos(phi / 2)] + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(theta / 2), -np.sin(theta / 2)], + [0, 0, np.sin(theta / 2), np.cos(theta / 2)], ], - dims = [[2,2], [2,2]] + dims=[[2, 2], [2, 2]], ) def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class CRZ(_ControlledTwoQubitGate): @@ -918,21 +924,25 @@ class CRZ(_ControlledTwoQubitGate): target_gate: Final[Type[Gate]] = RZ latex_str: Final[str] = r"{\rm CRZ}" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: - phi = arg_value[0] + theta = arg_value[0] return Qobj( - [[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, np.exp(-1j * phi / 2), 0], - [0, 0, 0, np.exp(1j * phi / 2)] + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-1j * theta / 2), 0], + [0, 0, 0, np.exp(1j * theta / 2)], ], - dims = [[2,2], [2,2]] + dims=[[2, 2], [2, 2]], ) def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class CPHASE(_ControlledTwoQubitGate): @@ -966,21 +976,25 @@ class CPHASE(_ControlledTwoQubitGate): target_gate: Final[Type[Gate]] = PHASE latex_str: Final[str] = r"{\rm CPHASE}" + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: - phi = arg_value[0] + theta = arg_value[0] return Qobj( - [[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, np.exp(1j * phi)] + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, np.exp(1j * theta)], ], - dims = [[2,2], [2,2]] + dims=[[2, 2], [2, 2]], ) def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) class CQASMU(_ControlledTwoQubitGate): @@ -998,19 +1012,38 @@ class CQASMU(_ControlledTwoQubitGate): target_gate: Final[Type[Gate]] = QASMU latex_str: Final[str] = r"{\rm CQASMU}" + def __init__( + self, + theta: float, + phi: float, + gamma: float, + arg_label: str | None = None, + ): + super().__init__(theta, phi, gamma, arg_label=arg_label) + @staticmethod @lru_cache(maxsize=128) def _compute_qobj(arg_value: tuple[float]) -> Qobj: theta, phi, gamma = arg_value return Qobj( - [[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), - -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2)], - [0, 0, np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), - np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2)]] - , dims = [[2,2], [2,2]] + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [ + 0, + 0, + np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), + -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2), + ], + [ + 0, + 0, + np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), + np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2), + ], + ], + dims=[[2, 2], [2, 2]], ) def get_qobj(self) -> Qobj: - return self._compute_qobj(tuple(self.arg_value)) + return self._compute_qobj(self.arg_value) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 0700d6b7c..6f4db9bf2 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -1,6 +1,6 @@ import inspect from abc import abstractmethod -from collections.abc import Iterable +from collections.abc import Sequence from qutip import Qobj from qutip_qip.operations import Gate @@ -69,13 +69,24 @@ def __init__(self, *args, arg_label: str | None = None): f"Requires {self.num_params} parameters, got {len(args)}" ) self.validate_params(args) - self._arg_value = args + self._arg_value = tuple(args) self.arg_label = arg_label @property def arg_value(self) -> tuple[any]: return self._arg_value + @arg_value.setter + def arg_value(self, new_args: Sequence): + if not isinstance(new_args, Sequence): + new_args = [new_args] + + if len(new_args) != self.num_params: + raise ValueError(f"Requires {self.num_params} parameters, got {len(new_args)}") + + self.validate_params(new_args) + self._arg_value = tuple(new_args) + @staticmethod @abstractmethod def validate_params(arg_value): diff --git a/src/qutip_qip/utils.py b/src/qutip_qip/utils.py index 8ab25eb05..45ac57109 100644 --- a/src/qutip_qip/utils.py +++ b/src/qutip_qip/utils.py @@ -4,7 +4,8 @@ from qutip import Qobj -__all__ = ['valid_unitary'] +__all__ = ["valid_unitary"] + def valid_unitary(gate, num_qubits): """Verifies input is a valid quantum gate i.e. unitary Qobj. diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 3f012949a..88f0a5870 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -131,9 +131,9 @@ def test_add_gate(self): qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) qc.add_gate(gates.H, targets=[3]) qc.add_gate(gates.SWAP, targets=[1, 4]) - qc.add_gate(gates.RY(np.pi/2), targets=4) - qc.add_gate(gates.RY(np.pi/2), targets=5) - qc.add_gate(gates.RX(-np.pi/2), targets=[3]) + qc.add_gate(gates.RY(np.pi / 2), targets=4) + qc.add_gate(gates.RY(np.pi / 2), targets=5) + qc.add_gate(gates.RX(-np.pi / 2), targets=[3]) # Test explicit gate addition assert qc.instructions[0].operation.name == "CX" @@ -151,15 +151,15 @@ def test_add_gate(self): # Test adding 1 qubit gate on [start, end] qubits assert qc.instructions[5].operation.name == "RY" assert qc.instructions[5].targets == (4,) - assert qc.instructions[5].operation.arg_value[0] == np.pi/2 + assert qc.instructions[5].operation.arg_value[0] == np.pi / 2 assert qc.instructions[6].operation.name == "RY" assert qc.instructions[6].targets == (5,) - assert qc.instructions[6].operation.arg_value[0] == np.pi/2 + assert qc.instructions[6].operation.arg_value[0] == np.pi / 2 # Test adding 1 qubit gate on qubits [3] assert qc.instructions[7].operation.name == "RX" assert qc.instructions[7].targets == (3,) - assert qc.instructions[7].operation.arg_value[0] == -np.pi/2 + assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): num_qubits = 1 From 97540f3f8cbd2ead77582b1e09afbe6b23bdd8e4 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 03:40:31 +0530 Subject: [PATCH 062/117] Added expanded option in inverse method fort Parametric Gates --- src/qutip_qip/operations/controlled.py | 23 +++++---- src/qutip_qip/operations/gateclass.py | 7 ++- src/qutip_qip/operations/gates/other_gates.py | 3 +- .../operations/gates/single_qubit_gate.py | 50 ++++++++++++++++--- .../operations/gates/two_qubit_gate.py | 25 ++++++++-- src/qutip_qip/operations/parametric.py | 13 +++-- 6 files changed, 93 insertions(+), 28 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 2ed6b204c..f7cf5e235 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -185,7 +185,7 @@ def get_qobj(cls_or_self) -> Qobj: ) @class_or_instance_method - def inverse(cls_or_self) -> Gate: + def inverse(cls_or_self) -> Gate | Type[Gate]: if isinstance(cls_or_self, type): return controlled( cls_or_self.target_gate.inverse(), @@ -193,14 +193,19 @@ def inverse(cls_or_self) -> Gate: cls_or_self.ctrl_value, ) - inverse_target_gate = cls_or_self._target_inst.inverse() - arg_value = inverse_target_gate.arg_value - inverse = controlled( - type(inverse_target_gate), - cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value, - ) - return inverse(*arg_value) + elif cls_or_self._target_inst.is_parametric(): + inverse_gate_class, param = cls_or_self._target_inst.inverse( + expanded=True + ) + inverse = controlled( + inverse_gate_class, + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + ) + return inverse(*param) + + else: + raise NotImplementedError @staticmethod def is_controlled() -> bool: diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index f4eb203f7..e2fcbeb9d 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABC, ABCMeta, abstractmethod import inspect +from typing import Type import numpy as np from qutip import Qobj @@ -242,7 +243,7 @@ def get_qobj() -> Qobj: pass @classmethod - def inverse(cls) -> Gate: + def inverse(cls) -> Type[Gate]: """ Return the inverse of the gate. @@ -282,7 +283,9 @@ def is_parametric() -> bool: return False -def unitary_gate(gate_name: str, U: Qobj, namespace: str = "custom") -> Gate: +def unitary_gate( + gate_name: str, U: Qobj, namespace: str = "custom" +) -> Type[Gate]: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index ff2ca01f0..811e96dd6 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -38,8 +38,7 @@ def get_qobj(self, num_qubits=None): N = 2**num_qubits return Qobj( - np.exp(1.0j * phase) - * sp.eye(N, N, dtype=complex, format="csr"), + np.exp(1.0j * phase) * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], ) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index fc93abed3..73d304f20 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -396,8 +396,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: dims=[[2], [2]], ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + if expanded: + return RX, (-theta,) return RX(-theta) @@ -434,8 +439,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + if expanded: + return RY, (-theta,) return RY(-theta) @@ -467,8 +477,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: phi = arg_value[0] return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + if expanded: + return RZ, (-theta,) return RZ(-theta) @@ -500,8 +515,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + if expanded: + return PHASE, (-theta,) return PHASE(-theta) @@ -551,9 +571,16 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float, float]]: + phi, theta = self.arg_value - return R(phi, -theta) + inverse_params = (phi, -theta) + + if expanded: + return R, inverse_params + return R(*inverse_params) class QASMU(_SingleQubitParametricGate): @@ -604,6 +631,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ] ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: + theta, phi, gamma = self.arg_value - return QASMU(-theta, -gamma, -phi) + inverse_param = (-theta, -gamma, -phi) + + if expanded: + return QASMU, inverse_param + return QASMU(*inverse_param) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 43567c40a..3604cb9a1 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -472,8 +472,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + alpha = self.arg_value[0] + if expanded: + return SWAPALPHA, (-alpha,) return SWAPALPHA(-alpha) @@ -534,9 +539,16 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float, float]]: + theta, phi = self.arg_value - return MS(-theta, phi) + inverse_param = (-theta, phi) + + if expanded: + return MS, inverse_param + return MS(*inverse_param) class RZX(_TwoQubitParametricGate): @@ -588,8 +600,13 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: dims=[[2, 2], [2, 2]], ) - def inverse(self) -> Gate: + def inverse( + self, expanded: bool = False + ) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + if expanded: + return RZX, (-theta,) return RZX(-theta) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 6f4db9bf2..321253782 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -80,10 +80,12 @@ def arg_value(self) -> tuple[any]: def arg_value(self, new_args: Sequence): if not isinstance(new_args, Sequence): new_args = [new_args] - + if len(new_args) != self.num_params: - raise ValueError(f"Requires {self.num_params} parameters, got {len(new_args)}") - + raise ValueError( + f"Requires {self.num_params} parameters, got {len(new_args)}" + ) + self.validate_params(new_args) self._arg_value = tuple(new_args) @@ -115,6 +117,11 @@ def get_qobj(self) -> Qobj: """ pass + def inverse(self, expanded: bool = False) -> Gate: + if self.self_inverse: + return self + raise NotImplementedError + @staticmethod def is_parametric(): return True From 46b4f9bcb76123bc284692584737bc01a6fe2d64 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 03:47:02 +0530 Subject: [PATCH 063/117] Corrected typhinting for tuple[] tuple[float] means a tuple with a single float element similarly tuple[float, float] is a tuple with 2 float elements. For arbitarily number of elements we need to use tuple[float, ...] --- src/qutip_qip/circuit/instruction.py | 8 ++++---- src/qutip_qip/operations/gates/single_qubit_gate.py | 4 ++-- src/qutip_qip/operations/gates/two_qubit_gate.py | 6 +++--- src/qutip_qip/operations/parametric.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index 2a40e7dee..f3db34a1d 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -18,8 +18,8 @@ def _validate_non_negative_int_tuple(T: any, txt: str = ""): @dataclass(frozen=True, slots=True) class CircuitInstruction(ABC): operation: Gate | Measurement - qubits: tuple[int] = tuple() - cbits: tuple[int] = tuple() + qubits: tuple[int, ...] = tuple() + cbits: tuple[int, ...] = tuple() style: dict = field(default_factory=dict) def __post_init__(self): @@ -96,13 +96,13 @@ def __post_init__(self) -> None: ) @property - def controls(self) -> tuple[int]: + def controls(self) -> tuple[int, ...]: if self.operation.is_controlled(): return self.qubits[: self.operation.num_ctrl_qubits] return () @property - def targets(self) -> tuple[int]: + def targets(self) -> tuple[int, ...]: if self.operation.is_controlled(): return self.qubits[self.operation.num_ctrl_qubits :] return self.qubits diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 73d304f20..071e19db5 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -22,7 +22,7 @@ class _SingleQubitParametricGate(AngleParametricGate): num_qubits: Final[int] = 1 @abstractmethod - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def _compute_qobj(arg_value: tuple[float, ...]) -> Qobj: pass def get_qobj(self) -> Qobj: @@ -556,7 +556,7 @@ def __init__(self, phi: float, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: phi, theta = arg_value return Qobj( [ diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 3604cb9a1..18820546b 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -36,7 +36,7 @@ class _TwoQubitParametricGate(AngleParametricGate): num_qubits: Final[int] = 2 @abstractmethod - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def _compute_qobj(arg_value: tuple[float, ...]) -> Qobj: pass def get_qobj(self) -> Qobj: @@ -517,7 +517,7 @@ def __init__(self, theta: float, phi: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: theta, phi = arg_value return Qobj( [ @@ -1040,7 +1040,7 @@ def __init__( @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def _compute_qobj(arg_value: tuple[float, float, float]) -> Qobj: theta, phi, gamma = arg_value return Qobj( [ diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 321253782..d4ae4e717 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -73,7 +73,7 @@ def __init__(self, *args, arg_label: str | None = None): self.arg_label = arg_label @property - def arg_value(self) -> tuple[any]: + def arg_value(self) -> tuple[any, ...]: return self._arg_value @arg_value.setter From 7b4cb9a0b2336756dd3084477139243602c0ef56 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 03:49:31 +0530 Subject: [PATCH 064/117] Add __hash__ method for Parametric Gates --- src/qutip_qip/operations/parametric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index d4ae4e717..73dabe06c 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -141,7 +141,7 @@ def __eq__(self, other) -> bool: return True def __hash__(self) -> None: - pass + return hash((type(self), self.arg_value)) class AngleParametricGate(ParametricGate): From a6f5d183204d3283fd5420926490c931976d273c Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 03:53:30 +0530 Subject: [PATCH 065/117] Added __eq__, __hash__ for Controlled Gates --- src/qutip_qip/operations/controlled.py | 14 ++++++++++++++ src/qutip_qip/operations/parametric.py | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index f7cf5e235..92e7eae95 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -219,6 +219,20 @@ def is_parametric(cls) -> bool: 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 for false for CRX(0.5), CRY(0.5) + if type(self) is not type(other): + return False + + # Returns for false for CRX(0.5), CRX(0.6) + if self._target_inst != other._target_inst: + return False + + return True + + def __hash__(self) -> int: + return hash((type(self), self._target_inst)) + def controlled( gate: Gate, diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 73dabe06c..583f78ccb 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -133,14 +133,17 @@ def __str__(self): """ def __eq__(self, other) -> bool: + # Returns for false for RX(0.5), RY(0.5) if type(self) is not type(other): return False + # Returns for false for RX(0.5), RX(0.6) if self.arg_value != other.arg_value: return False + return True - def __hash__(self) -> None: + def __hash__(self) -> int: return hash((type(self), self.arg_value)) From 11a889c05bdee8a47e732a199a2990d8115f5dcc Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 04:47:18 +0530 Subject: [PATCH 066/117] Remove __hash__ from Controlled, Parametric Gate as they are mutable --- src/qutip_qip/operations/controlled.py | 14 +++++--------- src/qutip_qip/operations/parametric.py | 15 ++++----------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 92e7eae95..0adc32282 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -127,15 +127,14 @@ def __setattr__(self, name, value) -> None: 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!) + # This is because Python currently doesn't support abstract class attributes. @property @abstractmethod def target_gate() -> Gate: pass - @property - def self_inverse(self) -> int: - return self.target_gate.self_inverse - @classmethod def _validate_control_value(cls) -> None: """ @@ -220,19 +219,16 @@ 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 for false for CRX(0.5), CRY(0.5) + # Returns false for CRX(0.5), CRY(0.5) if type(self) is not type(other): return False - # Returns for false for CRX(0.5), CRX(0.6) + # Returns false for CRX(0.5), CRX(0.6) if self._target_inst != other._target_inst: return False return True - def __hash__(self) -> int: - return hash((type(self), self._target_inst)) - def controlled( gate: Gate, diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 583f78ccb..831e394c1 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -64,12 +64,8 @@ def __init_subclass__(cls, **kwargs) -> None: ) def __init__(self, *args, arg_label: str | None = None): - if len(args) != self.num_params: - raise ValueError( - f"Requires {self.num_params} parameters, got {len(args)}" - ) - self.validate_params(args) - self._arg_value = tuple(args) + # This auto triggers a call to arg_value setter (where checks happen) + self.arg_value = args self.arg_label = arg_label @property @@ -133,19 +129,16 @@ def __str__(self): """ def __eq__(self, other) -> bool: - # Returns for false for RX(0.5), RY(0.5) + # Returns false for RX(0.5), RY(0.5) if type(self) is not type(other): return False - # Returns for false for RX(0.5), RX(0.6) + # Returns false for RX(0.5), RX(0.6) if self.arg_value != other.arg_value: return False return True - def __hash__(self) -> int: - return hash((type(self), self.arg_value)) - class AngleParametricGate(ParametricGate): __slots__ = () From 41e65d58e56553642d813164880b4cae03115666 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 06:04:20 +0530 Subject: [PATCH 067/117] Made _compute_qobj() abstract in Parametric Gate This allows user to inherit Parametric Gate class and he just needs to define num_qubits, num_params, validate_params method (if he is not inheriting from AngleParametricGate) and _compute_qobj method which is a static method and can also be cached --- src/qutip_qip/operations/gates/other_gates.py | 3 +++ src/qutip_qip/operations/gates/single_qubit_gate.py | 7 ------- src/qutip_qip/operations/gates/two_qubit_gate.py | 7 ------- src/qutip_qip/operations/parametric.py | 7 ++++++- tests/test_compiler.py | 4 ++-- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 811e96dd6..742549daa 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -31,6 +31,9 @@ def __init__(self, phase: float = 0.0): def __repr__(self): return f"Gate({self.name}, phase {self.arg_value[0]})" + def _compute_qobj(): + pass + def get_qobj(self, num_qubits=None): phase = self.arg_value[0] if num_qubits is None: diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 071e19db5..5793b60c7 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -21,13 +21,6 @@ class _SingleQubitParametricGate(AngleParametricGate): __slots__ = () num_qubits: Final[int] = 1 - @abstractmethod - def _compute_qobj(arg_value: tuple[float, ...]) -> Qobj: - pass - - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) - class X(_SingleQubitGate): """ diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 18820546b..06682cf3b 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -35,13 +35,6 @@ class _TwoQubitParametricGate(AngleParametricGate): __slots__ = () num_qubits: Final[int] = 2 - @abstractmethod - def _compute_qobj(arg_value: tuple[float, ...]) -> Qobj: - pass - - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) - class _ControlledTwoQubitGate(ControlledGate): """Abstract two-qubit Controlled Gate (both parametric and non-parametric).""" diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 831e394c1..cbddd4c12 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -101,7 +101,6 @@ def validate_params(arg_value): """ pass - @abstractmethod def get_qobj(self) -> Qobj: """ Get the QuTiP quantum object representation using the current parameters. @@ -111,6 +110,12 @@ def get_qobj(self) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the gate with the specific `arg_value`. """ + return self._compute_qobj(self.arg_value) + + @staticmethod + @abstractmethod + def _compute_qobj(args: tuple) -> "Qobj": + """Every child must implement this pure math helper.""" pass def inverse(self, expanded: bool = False) -> Gate: diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 8240db012..653f1f366 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -85,7 +85,7 @@ class U1(AngleParametricGate): num_params = 1 self_inverse = False - def get_qobj(self): + def _compute_qobj(self): pass class U2(AngleParametricGate): @@ -93,7 +93,7 @@ class U2(AngleParametricGate): num_params = 1 self_inverse = False - def get_qobj(self): + def _compute_qobj(): pass num_qubits = 2 From 7f050a1352b521731f72cd7f272f5ff3fe69d90f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 06:16:15 +0530 Subject: [PATCH 068/117] Renamed unitary_gate method to get_unitary_gate --- src/qutip_qip/algorithms/qpe.py | 4 ++-- src/qutip_qip/operations/__init__.py | 4 ++-- src/qutip_qip/operations/gateclass.py | 2 +- src/qutip_qip/qasm.py | 4 ++-- src/qutip_qip/vqa.py | 4 ++-- tests/test_gates.py | 4 ++-- tests/test_qpe.py | 8 ++++---- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 6b4ae0af2..fe6c1ecca 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,7 @@ import numpy as np from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import unitary_gate, controlled, Gate +from qutip_qip.operations import get_unitary_gate, controlled, Gate from qutip_qip.operations.gates import H @@ -64,7 +64,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): # Add controlled-U^power gate controlled_u = controlled( - gate=unitary_gate( + gate=get_unitary_gate( gate_name=f"U^{power}", namespace="qpe", U=U_power, diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index c07e15392..46784943c 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -8,7 +8,7 @@ gate_sequence_product, controlled_gate_unitary, ) -from .gateclass import Gate, unitary_gate +from .gateclass import Gate, get_unitary_gate from .parametric import ParametricGate, AngleParametricGate from .controlled import ControlledGate, controlled from .measurement import Measurement @@ -51,7 +51,7 @@ "Gate", "ParametricGate", "ControlledGate", - "unitary_gate", + "get_unitary_gate", "controlled", "AngleParametricGate", "Measurement", diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index e2fcbeb9d..780c3feda 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -283,7 +283,7 @@ def is_parametric() -> bool: return False -def unitary_gate( +def get_unitary_gate( gate_name: str, U: Qobj, namespace: str = "custom" ) -> Type[Gate]: """ diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index a6ff8414e..1f3e048c2 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -11,7 +11,7 @@ import numpy as np from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import unitary_gate +from qutip_qip.operations import get_unitary_gate import qutip_qip.operations.gates as gates __all__ = ["read_qasm", "save_qasm", "print_qasm", "circuit_to_qasm_str"] @@ -804,7 +804,7 @@ def _gate_add( if custom_gate_unitary is not None: # Instantiate the wrapper gate - gate_obj = unitary_gate( + gate_obj = get_unitary_gate( gate_name=gate_name, U=custom_gate_unitary, ) diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 6fb250dd7..113c55b6f 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -10,7 +10,7 @@ from scipy.linalg import expm_frechet from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import gate_sequence_product, unitary_gate, Gate +from qutip_qip.operations import gate_sequence_product, get_unitary_gate, Gate class VQA: @@ -144,7 +144,7 @@ def construct_circuit(self, angles): n = block.get_free_parameters_num() current_params = angles[i : i + n] if n > 0 else [] - gate_instance = unitary_gate( + gate_instance = get_unitary_gate( gate_name=block.name, namespace="vqa", U=block.get_unitary(current_params), diff --git a/tests/test_gates.py b/tests/test_gates.py index 2376b043e..4194d3cda 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -8,7 +8,7 @@ Gate, expand_operator, controlled, - unitary_gate, + get_unitary_gate, qubit_clifford_group, hadamard_transform, ) @@ -256,7 +256,7 @@ def test_two_qubit(self, gate, n_controls): expected = _tensor_with_entanglement(qubits, reference, [q1, q2]) assert _infidelity(test, expected) < 1e-12 - random_gate = unitary_gate("random", qutip.rand_unitary([2] * 1)) + random_gate = get_unitary_gate("random", qutip.rand_unitary([2] * 1)) RandomThreeQubitGate = controlled(random_gate, 2) @pytest.mark.parametrize( diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 96e91e692..b7d873b08 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -4,7 +4,7 @@ from qutip import Qobj, sigmaz, tensor from qutip_qip.algorithms.qpe import qpe -from qutip_qip.operations import controlled, unitary_gate +from qutip_qip.operations import controlled, get_unitary_gate class TestQPE(unittest.TestCase): @@ -14,11 +14,11 @@ class TestQPE(unittest.TestCase): def test_custom_gate(self): """ - Test if unitary_gate correctly stores and returns the quantum object + Test if get_unitary_gate correctly stores and returns the quantum object """ U = Qobj([[0, 1], [1, 0]]) - custom = unitary_gate(gate_name="custom", U=U) + custom = get_unitary_gate(gate_name="custom", U=U) qobj = custom.get_qobj() assert_((qobj - U).norm() < 1e-12) @@ -29,7 +29,7 @@ def test_controlled_unitary(self): U = Qobj([[0, 1], [1, 0]]) controlled_u = controlled( - gate=unitary_gate(gate_name="CU", U=U), + gate=get_unitary_gate(gate_name="CU", U=U), ) assert_equal(controlled_u.ctrl_value, 1) From ca44070f7f876554d3aa875480bf1dceae636ff6 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 3 Mar 2026 17:58:33 +0530 Subject: [PATCH 069/117] replace gate.name comparisions with actual gate comparision --- src/qutip_qip/circuit/_decompose.py | 12 +++-- src/qutip_qip/circuit/circuit.py | 47 +++++++++-------- src/qutip_qip/circuit/draw/mat_renderer.py | 7 +-- src/qutip_qip/circuit/draw/texrenderer.py | 15 +++--- src/qutip_qip/circuit/draw/text_renderer.py | 5 +- src/qutip_qip/operations/controlled.py | 5 +- src/qutip_qip/operations/gates/__init__.py | 1 - src/qutip_qip/qasm.py | 23 ++++----- src/qutip_qip/transpiler/chain.py | 56 ++++++++++----------- tests/test_bit_flip.py | 4 +- tests/test_circuit.py | 38 +++++++------- tests/test_qft.py | 14 +++--- tests/test_qpe.py | 5 +- tests/test_scheduler.py | 2 +- 14 files changed, 119 insertions(+), 115 deletions(-) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 23cc1a96d..4f4861fc1 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -14,6 +14,8 @@ SQRTSWAP, SQRTISWAP, CZ, + CNOT, + SWAP, ) __all__ = ["_resolve_to_universal", "_resolve_2q_basis"] @@ -317,7 +319,7 @@ def _basis_CZ(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CNOT or gate == CX: qc_temp.add_gate( gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, @@ -349,7 +351,7 @@ def _basis_ISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CNOT or gate == CX: qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( @@ -374,7 +376,7 @@ def _basis_ISWAP(qc_temp, temp_resolved): targets=targets, ) - elif gate.name == "SWAP": + elif gate == SWAP: qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( @@ -410,7 +412,7 @@ def _basis_SQRTSWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CNOT or gate == CX: qc_temp.add_gate( gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, @@ -451,7 +453,7 @@ def _basis_SQRTISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CNOT or gate == CX: qc_temp.add_gate( gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 8c8d6280c..dafc25cc2 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -16,7 +16,7 @@ ) from qutip_qip.circuit.utils import _check_iterable, _check_limit_ from qutip_qip.operations import Gate, Measurement, expand_operator -from qutip_qip.operations.gates import RX, RY, RZ, GLOBALPHASE, GATE_CLASS_MAP +from qutip_qip.operations import gates as std from qutip_qip.typing import IntList try: @@ -116,7 +116,6 @@ def gates(self) -> list[CircuitInstruction]: return self._instructions gates.setter - def gates(self) -> None: warnings.warn( "QubitCircuit.gates has been replaced with QubitCircuit.instructions", @@ -306,7 +305,7 @@ def add_gate( stacklevel=2, ) - if isinstance(gate, GLOBALPHASE): + if isinstance(gate, std.GLOBALPHASE): self.add_global_phase(gate.arg_value[0]) return @@ -340,13 +339,13 @@ def add_gate( # This conditional block can be remove if the gate input is only # restricted to Gate subclasses or object instead of strings in the future. if not isinstance(gate, Gate): - if type(gate) is str and gate in GATE_CLASS_MAP: + if type(gate) is str and gate in std.GATE_CLASS_MAP: warnings.warn( "Passing Gate as a string input has been deprecated and will be removed in future versions.", DeprecationWarning, stacklevel=2, ) - gate_class = GATE_CLASS_MAP[gate] + gate_class = std.GATE_CLASS_MAP[gate] elif issubclass(gate, Gate): gate_class = gate @@ -663,15 +662,15 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name in ("X", "Y", "Z"): + if gate in (std.X, std.Y, std.Z): temp_resolved.add_global_phase(phase=np.pi / 2) - if gate.name == "X": - temp_resolved.add_gate(RX(np.pi), targets=targets) - elif gate.name == "Y": - temp_resolved.add_gate(RY(np.pi), targets=targets) + if gate == std.X: + temp_resolved.add_gate(std.RX(np.pi), targets=targets) + elif gate == std.Y: + temp_resolved.add_gate(std.RY(np.pi), targets=targets) else: - temp_resolved.add_gate(RZ(np.pi), targets=targets) + temp_resolved.add_gate(std.RZ(np.pi), targets=targets) else: try: @@ -714,45 +713,45 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "RX" and "RX" not in basis_1q: + if isinstance(gate, std.RX) and "RX" not in basis_1q: qc_temp.add_gate( - RY(-half_pi, arg_label=r"-\pi/2"), + std.RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RZ(gate.arg_value[0], arg_label=gate.arg_label), + std.RZ(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RY(-half_pi, arg_label=r"\pi/2"), + std.RY(-half_pi, arg_label=r"\pi/2"), targets=targets, ) - elif gate.name == "RY" and "RY" not in basis_1q: + elif isinstance(gate, std.RY) and "RY" not in basis_1q: qc_temp.add_gate( - RZ(-half_pi, arg_label=r"-\pi/2"), + std.RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RX(gate.arg_value[0], arg_label=gate.arg_label), + std.RX(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RZ(half_pi, arg_label=r"\pi/2"), + std.RZ(half_pi, arg_label=r"\pi/2"), targets=targets, ) - elif gate.name == "RZ" and "RZ" not in basis_1q: + elif isinstance(gate, std.RZ) and "RZ" not in basis_1q: qc_temp.add_gate( - RX(-half_pi, arg_label=r"-\pi/2"), + std.RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RY(gate.arg_value[0], arg_label=gate.arg_label), + std.RY(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RX(half_pi, arg_label=r"\pi/2"), + std.RX(half_pi, arg_label=r"\pi/2"), targets=targets, ) else: @@ -821,7 +820,7 @@ def propagators(self, expand=True, ignore_measurement=False): # For Circuit's Global Phase qobj = Qobj([self.global_phase]) if expand: - qobj = GLOBALPHASE(self.global_phase).get_qobj( + qobj = std.GLOBALPHASE(self.global_phase).get_qobj( num_qubits=self.num_qubits ) diff --git a/src/qutip_qip/circuit/draw/mat_renderer.py b/src/qutip_qip/circuit/draw/mat_renderer.py index c707e86ea..3e49fdf8d 100644 --- a/src/qutip_qip/circuit/draw/mat_renderer.py +++ b/src/qutip_qip/circuit/draw/mat_renderer.py @@ -15,6 +15,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import BaseRenderer, StyleConfig from qutip_qip.operations import Gate +from qutip_qip.operations import gates as std class MatRenderer(BaseRenderer): @@ -543,7 +544,7 @@ def _draw_multiq_gate( ) com_xskip = self._get_xskip(wire_list, layer) - if gate.name == "CNOT" or gate.name == "CX": + if gate in [std.CNOT, std.CX]: self._draw_control_node(controls[0], com_xskip, self.color) self._draw_target_node(targets[0], com_xskip, self.color) self._draw_qbridge(targets[0], controls[0], com_xskip, self.color) @@ -554,7 +555,7 @@ def _draw_multiq_gate( com_xskip, ) - elif gate.name == "SWAP": + elif gate == std.SWAP: self._draw_swap_mark(targets[0], com_xskip, self.color) self._draw_swap_mark(targets[1], com_xskip, self.color) self._draw_qbridge(targets[0], targets[1], com_xskip, self.color) @@ -565,7 +566,7 @@ def _draw_multiq_gate( com_xskip, ) - elif gate.name == "TOFFOLI": + elif gate == std.TOFFOLI: self._draw_control_node(controls[0], com_xskip, self.color) self._draw_control_node(controls[1], com_xskip, self.color) self._draw_target_node(targets[0], com_xskip, self.color) diff --git a/src/qutip_qip/circuit/draw/texrenderer.py b/src/qutip_qip/circuit/draw/texrenderer.py index ae3d6a42c..378089e87 100644 --- a/src/qutip_qip/circuit/draw/texrenderer.py +++ b/src/qutip_qip/circuit/draw/texrenderer.py @@ -8,6 +8,7 @@ from typing import Callable from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations import gates as std # As a general note wherever you see {{}} in a python rf string that represents a {} @@ -71,7 +72,7 @@ def latex_code(self) -> str: for n in range(self.num_qubits + self.num_cbits): if targets and n in targets: if len(targets) > 1: - if gate.name == "SWAP": + if gate == std.SWAP: if _swap_processing: col.append(r" \qswap \qw") continue @@ -98,17 +99,17 @@ def latex_code(self) -> str: rf" \ghost{{{self._gate_label(gate)}}} " ) - elif gate.name == "CNOT" or gate.name == "CX": + elif gate in [std.CNOT, std.CX]: col.append(r" \targ ") - elif gate.name == "CY": + elif gate == std.CY: col.append(r" \targ ") - elif gate.name == "CZ": + elif gate == std.CZ: col.append(r" \targ ") - elif gate.name == "CS": + elif gate == std.CS: col.append(r" \targ ") - elif gate.name == "CT": + elif gate == std.CT: col.append(r" \targ ") - elif gate.name == "TOFFOLI": + elif gate == std.TOFFOLI: col.append(r" \targ ") else: col.append(rf" \gate{{{self._gate_label(gate)}}} ") diff --git a/src/qutip_qip/circuit/draw/text_renderer.py b/src/qutip_qip/circuit/draw/text_renderer.py index 0578b0623..55bdabf84 100644 --- a/src/qutip_qip/circuit/draw/text_renderer.py +++ b/src/qutip_qip/circuit/draw/text_renderer.py @@ -7,6 +7,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import BaseRenderer, StyleConfig from qutip_qip.operations import Gate +from qutip_qip.operations import gates as std class TextRenderer(BaseRenderer): @@ -443,7 +444,7 @@ def layout(self) -> None: if gate.is_parametric() and gate.arg_label is not None: gate_text = gate.arg_label - if gate.name == "SWAP": + if gate == std.SWAP: wire_list = list(range(min(targets), max(targets) + 1)) width = 4 * ceil(self.style.gate_pad) + 1 else: @@ -483,7 +484,7 @@ def layout(self) -> None: self._update_cbridge(qubits, cbits, wire_list, width) elif circ_instruction.is_gate_instruction(): - if gate.name == "SWAP": + if gate == std.SWAP: self._update_swap_gate(wire_list) else: self._update_target_multiq( diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 0adc32282..0bacfa376 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -192,7 +192,10 @@ def inverse(cls_or_self) -> Gate | Type[Gate]: cls_or_self.ctrl_value, ) - elif cls_or_self._target_inst.is_parametric(): + elif ( + isinstance(cls_or_self, object) + and cls_or_self._target_inst.is_parametric() + ): inverse_gate_class, param = cls_or_self._target_inst.inverse( expanded=True ) diff --git a/src/qutip_qip/operations/gates/__init__.py b/src/qutip_qip/operations/gates/__init__.py index 61d61f518..1cfd97f8e 100644 --- a/src/qutip_qip/operations/gates/__init__.py +++ b/src/qutip_qip/operations/gates/__init__.py @@ -82,7 +82,6 @@ "BERKELEY": BERKELEY, "BERKELEYdag": BERKELEYdag, "SWAPALPHA": SWAPALPHA, - "SWAPalpha": SWAPALPHA, "MS": MS, "TOFFOLI": TOFFOLI, "FREDKIN": FREDKIN, diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 1f3e048c2..730f6231d 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -283,10 +283,8 @@ def _initialize_pass(self): elif command[0] == "}": if not curr_gate.gates_inside: raise NotImplementedError( - "QASM: opaque gate {} are \ - not allowed, please define \ - or omit \ - them".format(curr_gate.name) + f"QASM: opaque gate {curr_gate.name} are \ + not allowed, please define or omit them" ) open_bracket_mode = False self.gate_names.add(curr_gate.name) @@ -1016,25 +1014,26 @@ def _qasm_defns(self, gate): QuTiP gate which needs to be defined in QASM format. """ - if gate.name == "CRY": + if isinstance(gate, gates.CRY): gate_def = "gate cry(theta) a,b { cu3(theta,0,0) a,b; }" - elif gate.name == "CRX": + elif isinstance(gate, gates.CRX): gate_def = "gate crx(theta) a,b { cu3(theta,-pi/2,pi/2) a,b; }" - elif gate.name == "SQRTX": + elif gate == gates.SQRTX: gate_def = "gate sqrtnot a {h a; u1(-pi/2) a; h a; }" - elif gate.name == "CZ": + elif gate == gates.CZ: gate_def = "gate cz a,b { cu1(pi) a,b; }" - elif gate.name == "CS": + elif gate == gates.CS: gate_def = "gate cs a,b { cu1(pi/2) a,b; }" - elif gate.name == "CT": + elif gate == gates.CT: gate_def = "gate ct a,b { cu1(pi/4) a,b; }" - elif gate.name == "SWAP": + elif gate == gates.SWAP: gate_def = "gate swap a,b { cx a,b; cx b,a; cx a,b; }" else: + print(gate) err_msg = f"No definition specified for {gate.name} gate" raise NotImplementedError(err_msg) - self.output("// QuTiP definition for gate {}".format(gate.name)) + self.output(f"// QuTiP definition for gate {gate.name}") self.output(gate_def) self.gate_name_map[gate.name] = gate.name.lower() diff --git a/src/qutip_qip/transpiler/chain.py b/src/qutip_qip/transpiler/chain.py index a9e50807c..760076359 100644 --- a/src/qutip_qip/transpiler/chain.py +++ b/src/qutip_qip/transpiler/chain.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.gates import SWAP +from qutip_qip.operations import gates as std def to_chain_structure(qc: QubitCircuit, setup="linear"): @@ -29,12 +29,12 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): qc_t = QubitCircuit(N) qc_t.add_global_phase(qc.global_phase) swap_gates = [ - SWAP, - "ISWAP", - "SQRTISWAP", - "SQRTSWAP", - "BERKELEY", - "SWAPalpha", + std.SWAP, + std.ISWAP, + std.SQRTISWAP, + std.SQRTSWAP, + std.BERKELEY, + std.SWAPALPHA, ] for circ_instruction in qc.instructions: @@ -42,7 +42,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls targets = circ_instruction.targets - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: start = min([targets[0], controls[0]]) end = max([targets[0], controls[0]]) @@ -67,7 +67,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): # then the required gate if and then another swap # if control and target have one qubit between # them, provided |control-target| is odd. - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) if end == controls[0]: qc_t.add_gate( gate, targets=[i + 1], controls=[i + 2] @@ -76,15 +76,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): qc_t.add_gate( gate, targets=[i + 2], controls=[i + 1] ) - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: # Swap the target/s and/or control with their # adjacent qubit to bring them closer. - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate( - SWAP, + std.SWAP, targets=[start + end - i - 1, start + end - i], ) i += 1 @@ -112,7 +112,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): N + start - end - i - i == 2 and (N - end + start + 1) % 2 == 1 ): - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) if end == controls[0]: temp.add_gate( gate, targets=[i + 2], controls=[i + 1] @@ -121,13 +121,13 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): temp.add_gate( gate, targets=[i + 1], controls=[i + 2] ) - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate( - SWAP, + std.SWAP, targets=[ N + start - end - i - 1, N + start - end - i, @@ -143,7 +143,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls if j < N - end - 2: - if gate.name in ["CNOT", "CX" "CSIGN", "CZ"]: + if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -158,7 +158,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) elif j == N - end - 2: - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -173,7 +173,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) else: - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=(end + targets[0]) % N, @@ -192,7 +192,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): elif (end - start) == N - 1: qc_t.add_gate(gate, targets=targets, controls=controls) - elif gate.name in swap_gates: + elif gate in swap_gates: start = min(targets) end = max(targets) @@ -207,15 +207,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): elif (start + end - i - i) == 2 and ( end - start + 1 ) % 2 == 1: - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate(gate, targets=[i + 1, i + 2]) - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate( - SWAP, + std.SWAP, targets=[start + end - i - 1, start + end - i], ) i += 1 @@ -234,15 +234,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): N + start - end - i - i == 2 and (N - end + start + 1) % 2 == 1 ): - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate(gate, targets=[i + 1, i + 2]) - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate( - SWAP, + std.SWAP, targets=[ N + start - end - i - 1, N + start - end - i, diff --git a/tests/test_bit_flip.py b/tests/test_bit_flip.py index 7089f789f..3aa3949ca 100644 --- a/tests/test_bit_flip.py +++ b/tests/test_bit_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import BitFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations.gates import X +from qutip_qip.operations.gates import X, CX @pytest.fixture @@ -25,7 +25,7 @@ def test_encode_circuit_structure(code, data_qubits): code.encode_circuit(qc, data_qubits) assert len(qc.instructions) == 2 - assert qc.instructions[0].operation.name == "CX" + assert qc.instructions[0].operation == CX assert qc.instructions[0].controls == (0,) assert qc.instructions[0].targets == (1,) assert qc.instructions[1].controls == (0,) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 88f0a5870..6522fed42 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -136,28 +136,28 @@ def test_add_gate(self): qc.add_gate(gates.RX(-np.pi / 2), targets=[3]) # Test explicit gate addition - assert qc.instructions[0].operation.name == "CX" + assert qc.instructions[0].operation == gates.CX assert qc.instructions[0].targets == (1,) assert qc.instructions[0].controls == (0,) # Test direct gate addition - assert qc.instructions[1].operation.name == "SWAP" + assert qc.instructions[1].operation == gates.SWAP assert qc.instructions[1].targets == (1, 4) # Test specified position gate addition - assert qc.instructions[3].operation.name == "H" + assert qc.instructions[3].operation == gates.H assert qc.instructions[3].targets == (3,) # Test adding 1 qubit gate on [start, end] qubits - assert qc.instructions[5].operation.name == "RY" + assert isinstance(qc.instructions[5].operation, gates.RY) assert qc.instructions[5].targets == (4,) assert qc.instructions[5].operation.arg_value[0] == np.pi / 2 - assert qc.instructions[6].operation.name == "RY" + assert isinstance(qc.instructions[6].operation, gates.RY) assert qc.instructions[6].targets == (5,) assert qc.instructions[6].operation.arg_value[0] == np.pi / 2 # Test adding 1 qubit gate on qubits [3] - assert qc.instructions[7].operation.name == "RX" + assert isinstance(qc.instructions[7].operation, gates.RX) assert qc.instructions[7].targets == (3,) assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 @@ -322,7 +322,7 @@ def test_add_measurement(self): assert qc.instructions[5].cbits[0] == 2 # checking if gates are added correctly with measurements - assert qc.instructions[2].operation.name == "TOFFOLI" + assert qc.instructions[2].operation == gates.TOFFOLI assert qc.instructions[4].cbits == (0, 1) @pytest.mark.parametrize( @@ -351,15 +351,15 @@ def test_single_qubit_gates(self): qc.add_gate(gates.S, targets=[1]) qc.add_gate(gates.T, targets=[2]) - assert qc.instructions[8].operation.name == "T" - assert qc.instructions[7].operation.name == "S" - assert qc.instructions[6].operation.name == "CZ" - assert qc.instructions[5].operation.name == "CT" - assert qc.instructions[4].operation.name == "Z" - assert qc.instructions[3].operation.name == "CS" - assert qc.instructions[2].operation.name == "Y" - assert qc.instructions[1].operation.name == "CY" - assert qc.instructions[0].operation.name == "X" + assert qc.instructions[8].operation == gates.T + assert qc.instructions[7].operation == gates.S + assert qc.instructions[6].operation == gates.CZ + assert qc.instructions[5].operation == gates.CT + assert qc.instructions[4].operation == gates.Z + assert qc.instructions[3].operation == gates.CS + assert qc.instructions[2].operation == gates.Y + assert qc.instructions[1].operation == gates.CY + assert qc.instructions[0].operation == gates.X assert qc.instructions[8].targets == (2,) assert qc.instructions[7].targets == (1,) @@ -394,10 +394,10 @@ def test_reverse(self): qc_rev = qc.reverse_circuit() - assert qc_rev.instructions[0].operation.name == "H" + assert qc_rev.instructions[0].operation == gates.H assert qc_rev.instructions[1].operation.name == "M1" - assert qc_rev.instructions[2].operation.name == "CX" - assert qc_rev.instructions[3].operation.name == "RX" + assert qc_rev.instructions[2].operation == gates.CX + assert isinstance(qc_rev.instructions[3].operation, gates.RX) assert qc_rev.input_states[0] == "0" assert qc_rev.input_states[2] is None diff --git a/tests/test_qft.py b/tests/test_qft.py index 4c11118dc..d87750069 100644 --- a/tests/test_qft.py +++ b/tests/test_qft.py @@ -1,6 +1,8 @@ -from numpy.testing import assert_, assert_equal, assert_string_equal +from numpy.testing import assert_, assert_equal + from qutip_qip.algorithms.qft import qft, qft_steps, qft_gate_sequence from qutip_qip.operations import gate_sequence_product +from qutip_qip.operations.gates import CPHASE, H, SWAP class TestQFT: @@ -29,11 +31,11 @@ def testQFTGateSequenceNoSwapping(self): totsize = N * (N + 1) / 2 assert_equal(len(circuit.instructions), totsize) - snots = sum(g.operation.name == "H" for g in circuit.instructions) + snots = sum(g.operation == H for g in circuit.instructions) assert_equal(snots, N) phases = sum( - g.operation.name == "CPHASE" for g in circuit.instructions + isinstance(g.operation, CPHASE) for g in circuit.instructions ) assert_equal(phases, N * (N - 1) / 2) @@ -50,9 +52,7 @@ def testQFTGateSequenceWithSwapping(self): assert_equal(len(circuit.instructions), phases + swaps) for i in range(phases, phases + swaps): - assert_string_equal( - circuit.instructions[i].operation.name, "SWAP" - ) + assert circuit.instructions[i].operation == SWAP def testQFTGateSequenceWithCNOT(self): """ @@ -63,5 +63,5 @@ def testQFTGateSequenceWithCNOT(self): circuit = qft_gate_sequence(N, swapping=False, to_cnot=True) assert not any( - [ins.operation.name == "CPHASE" for ins in circuit.instructions] + [isinstance(ins.operation, CPHASE) for ins in circuit.instructions] ) diff --git a/tests/test_qpe.py b/tests/test_qpe.py index b7d873b08..f7f9d93b8 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -5,6 +5,7 @@ from qutip_qip.algorithms.qpe import qpe from qutip_qip.operations import controlled, get_unitary_gate +from qutip_qip.operations import gates as std class TestQPE(unittest.TestCase): @@ -127,11 +128,9 @@ def test_qpe_to_cnot_flag(self): num_counting = 2 circuit1 = qpe(U, num_counting_qubits=num_counting, to_cnot=False) - circuit2 = qpe(U, num_counting_qubits=num_counting, to_cnot=True) - has_cnot = any( - gate.operation.name == "CX" for gate in circuit2.instructions + gate.operation == std.CX for gate in circuit2.instructions ) assert_(has_cnot) assert_(len(circuit2.instructions) > len(circuit1.instructions)) diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index f7d7eea80..8791a355e 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -121,7 +121,7 @@ def _instructions1(): instruction_list = [] for circuit_ins in circuit3.instructions: - if circuit_ins.operation.name == "H": + if circuit_ins.operation == H: instruction_list.append(PulseInstruction(circuit_ins, duration=1)) else: instruction_list.append(PulseInstruction(circuit_ins, duration=2)) From 4d63b1f721ae3766f380a043b42a58cb256db0ef Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 03:07:52 +0530 Subject: [PATCH 070/117] Added checks for input for add_gate method --- src/qutip_qip/circuit/circuit.py | 153 ++++++++++++++++--------------- src/qutip_qip/circuit/utils.py | 61 ++++++++---- src/qutip_qip/typing.py | 13 ++- 3 files changed, 128 insertions(+), 99 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index dafc25cc2..471b67490 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -3,7 +3,7 @@ """ import warnings -from typing import Iterable +from typing import Iterable, Type from qutip import qeye, Qobj import numpy as np @@ -14,10 +14,10 @@ GateInstruction, MeasurementInstruction, ) -from qutip_qip.circuit.utils import _check_iterable, _check_limit_ +from qutip_qip.circuit.utils import check_limit, convert_int_to_list from qutip_qip.operations import Gate, Measurement, expand_operator from qutip_qip.operations import gates as std -from qutip_qip.typing import IntList +from qutip_qip.typing import Int, IntSequence try: from IPython.display import Image as DisplayImage, SVG as DisplaySVG @@ -98,6 +98,29 @@ def __init__( "To define custom gates refer to this example in documentation " ) + @property + def num_qubits(self) -> int: + """ + Number of qubits in the circuit. + """ + return self._num_qubits + + @property + def N(self) -> int: + """ + Number of qubits in the circuit. + """ + warnings.warn( + "The 'N' parameter is deprecated. Please use " + "'num_qubits' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self._num_qubits + + def __repr__(self) -> str: + return "" + @property def global_phase(self): return self._global_phase @@ -115,6 +138,7 @@ def gates(self) -> list[CircuitInstruction]: ) return self._instructions + # fmt: off gates.setter def gates(self) -> None: warnings.warn( @@ -122,47 +146,16 @@ def gates(self) -> None: DeprecationWarning, stacklevel=2, ) + # fmt: on @property def instructions(self) -> list[CircuitInstruction]: return self._instructions - @property - def num_qubits(self) -> int: - """ - Number of qubits in the circuit. - """ - return self._num_qubits - - @property - def N(self) -> int: - """ - Number of qubits in the circuit. - """ - warnings.warn( - "The 'N' parameter is deprecated. Please use " - "'num_qubits' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._num_qubits - - def __repr__(self) -> str: - return "" - - def _repr_png_(self) -> None: - """ - Provide PNG representation for Jupyter Notebook. - """ - try: - self.draw(renderer="matplotlib") - except ImportError: - self.draw("text") - def add_state( self, state: str, - targets: IntList, + targets: IntSequence, state_type: str = "input", # FIXME Add an enum type hinting? ): """ @@ -196,7 +189,7 @@ def add_state( def add_measurement( self, measurement: str | Measurement, - targets: int | IntList, + targets: int | IntSequence, classical_store: int, index: None = None, ): @@ -246,15 +239,15 @@ def add_measurement( def add_gate( self, - gate: Gate | str, + gate: Gate | Type[Gate] | str, targets: int | Iterable[int] = (), controls: int | Iterable[int] = (), - arg_value: None = None, - arg_label: str | None = None, - control_value: None = None, classical_controls: int | Iterable[int] = (), classical_control_value: int | None = None, style: dict = None, + arg_value: None = None, + arg_label: None = None, + control_value: None = None, index: None = None, ): """ @@ -262,7 +255,7 @@ def add_gate( Parameters ---------- - gate: string or :class:`~.operations.Gate` + gate: :class:`~.operations.Gate` or :obj:`~.operations.Gate` or str Gate name. If gate is an instance of :class:`~.operations.Gate`, parameters are unpacked and added. targets: int or list, optional @@ -286,6 +279,7 @@ def add_gate( style: For circuit draw """ + # Deprecation warnings if index is not None: raise ValueError("argument index is no longer supported") @@ -309,33 +303,20 @@ def add_gate( self.add_global_phase(gate.arg_value[0]) return - # Handling case for int input (TODO use try except) - targets = [targets] if type(targets) is int else targets - controls = [controls] if type(controls) is int else controls - classical_controls = ( - [classical_controls] - if type(classical_controls) is int - else classical_controls + # Handling case for integer input + targets = convert_int_to_list("targets", targets) + controls = convert_int_to_list("controls", controls) + classical_controls = convert_int_to_list( + "classical_controls", classical_controls ) - # This will raise an error if not an iterable type (e.g. list, tuple, etc.) - _check_iterable("targets", targets) - _check_iterable("controls", controls) - _check_iterable("classical_controls", classical_controls) - - # Checks each element is of given type (e.g. int) and within the limit - _check_limit_("targets", targets, self.num_qubits - 1, int) - _check_limit_("controls", controls, self.num_qubits - 1, int) - _check_limit_( - "classical_controls", classical_controls, self.num_cbits - 1, int + # Checks each element within the limit + check_limit("targets", targets, 0, self.num_qubits - 1) + check_limit("controls", controls, 0, self.num_qubits - 1) + check_limit( + "classical_controls", classical_controls, 0, self.num_cbits - 1 ) - # Check len(controls) == gate.num_ctrl_qubits - - # Default value for classical control - if len(classical_controls) > 0 and classical_control_value is None: - classical_control_value = 2 ** (len(classical_controls)) - 1 - # This conditional block can be remove if the gate input is only # restricted to Gate subclasses or object instead of strings in the future. if not isinstance(gate, Gate): @@ -361,6 +342,19 @@ def add_gate( else: gate = gate_class + # Check for gates + if not (isinstance(gate, Gate) or issubclass(gate, Gate)): + raise TypeError(f"gate must be of type Gate, got {gate}") + elif gate.is_parametric() and (not isinstance(gate, Gate)): + raise TypeError( + "You must pass an instantiated object for a Parametrized Gate" + ) + elif (not gate.is_parametric()) and (not issubclass(gate, Gate)): + raise TypeError( + "You must pass a Gate type for a non-parametrized gate" + ) + + # Check len(controls) == gate.num_ctrl_qubits if gate.is_controlled() and len(controls) != gate.num_ctrl_qubits: raise ValueError( f"{gate.name} takes {gate.num_ctrl_qubits} qubits, but {len(controls)} were provided." @@ -371,20 +365,35 @@ def add_gate( f"{gate.name} takes {gate.num_qubits} qubits, but {len(controls) + len(targets)} were provided." ) + # Check for classical control + default_classical_ctrl_val = 2 ** (len(classical_controls)) - 1 + + if classical_control_value is None: + if len(classical_controls) > 0: + classical_control_value = default_classical_ctrl_val + + elif not isinstance(classical_control_value, Int): + raise TypeError( + f"classical_control_value must be an integer or None, got {classical_control_value}" + ) + + elif ( + classical_control_value < 0 + or classical_control_value > default_classical_ctrl_val + ): + raise ValueError( + f"{classical_control_value} must be with [0, {default_classical_ctrl_val}]" + ) + qubits = [] - if controls is not None: - qubits.extend(controls) + qubits.extend(controls) qubits.extend(targets) - cbits = tuple() - if classical_controls is not None: - cbits = tuple(classical_controls) - self._instructions.append( GateInstruction( operation=gate, qubits=tuple(qubits), - cbits=cbits, + cbits=tuple(classical_controls), cbits_ctrl_value=classical_control_value, style=style, ) diff --git a/src/qutip_qip/circuit/utils.py b/src/qutip_qip/circuit/utils.py index 2d4809d0e..ffa2d2da8 100644 --- a/src/qutip_qip/circuit/utils.py +++ b/src/qutip_qip/circuit/utils.py @@ -1,25 +1,46 @@ -from typing import Iterable +from typing import TypeVar, Sequence +import numpy as np +from qutip_qip.typing import Int, IntSequence +T = TypeVar("T") # T can be any type -def _check_iterable(input_name: str, input_value: any): - try: - iter(input_value) - except TypeError: - raise TypeError( - f"{input_name} must be an iterable input, got {input_value}." + +def check_limit( + input_name: str, input_value: Sequence[T], lower_limit: T, upper_limit: T +): + if len(input_value) == 0: + return + + min_element = min(input_value) + if min_element < lower_limit: + raise ValueError( + f"Each entry of {input_name} must be greater than {lower_limit}, but found {min_element}." + ) + + max_element = max(input_value) + if max_element > upper_limit: + raise ValueError( + f"Each entry of {input_name} must be less than {upper_limit}, but found {max_element}." ) -def _check_limit_( - input_name: str, input_value: Iterable, limit, element_type: type = int -): - for e in input_value: - if type(e) is not element_type: - raise TypeError( - f"Each entry of {input_name} must be less than {limit}, got {input_value}." - ) - - if e > limit: - raise ValueError( - f"Each entry of {input_name} must be less than {limit}, got {input_value}." - ) +def convert_int_to_list( + input_name: str, input_value: Int | IntSequence +) -> IntSequence: + if isinstance(input_value, Int): + return [input_value] + + elif isinstance(input_value, Sequence) and not isinstance( + input_value, str + ): + for i, val in enumerate(input_value): + if not isinstance(val, Int): + raise TypeError( + f"All elements in '{input_name}' must be integers. " + f"Found {type(val).__name__} ({val}) at index {i}." + ) + return input_value + else: + raise TypeError( + f"{input_name} must be an int or sequence of int, got {input_value}." + ) diff --git a/src/qutip_qip/typing.py b/src/qutip_qip/typing.py index c1eb3148f..b9ed57736 100644 --- a/src/qutip_qip/typing.py +++ b/src/qutip_qip/typing.py @@ -6,10 +6,9 @@ "Int", "Real", "Number", - "ScalarList", - "IntList", - "RealList", - "ScalarList", + "IntSequence", + "RealSequence", + "ScalarSequence", "ArrayLike", ] @@ -19,8 +18,8 @@ Real: TypeAlias = int | float | np.integer | np.floating Number: TypeAlias = int | float | complex | np.number -IntList = Sequence[Int] -RealList = Sequence[Real] -ScalarList = Sequence[Number] +IntSequence = Sequence[Int] +RealSequence = Sequence[Real] +ScalarSequence = Sequence[Number] ArrayLike = Sequence[any] | np.ndarray From 242d5fb56940adde7525ea7bdfff0211fe901f59 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 03:39:22 +0530 Subject: [PATCH 071/117] Corrected inconsistency bt Gate and Type[Gate] --- src/qutip_qip/circuit/draw/mat_renderer.py | 3 ++- src/qutip_qip/circuit/draw/text_renderer.py | 5 +++-- src/qutip_qip/circuit/instruction.py | 5 +++-- src/qutip_qip/operations/controlled.py | 6 +++--- src/qutip_qip/operations/gates/single_qubit_gate.py | 1 - src/qutip_qip/operations/gates/two_qubit_gate.py | 1 - src/qutip_qip/qasm.py | 5 +++-- src/qutip_qip/qiskit/utils/converter.py | 5 +++-- src/qutip_qip/qiskit/utils/target_gate_set.py | 4 ++-- src/qutip_qip/vqa.py | 2 +- tests/test_gates.py | 7 ++++--- 11 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/qutip_qip/circuit/draw/mat_renderer.py b/src/qutip_qip/circuit/draw/mat_renderer.py index 3e49fdf8d..ba25b7ece 100644 --- a/src/qutip_qip/circuit/draw/mat_renderer.py +++ b/src/qutip_qip/circuit/draw/mat_renderer.py @@ -2,6 +2,7 @@ Module for rendering a quantum circuit using matplotlib library. """ +from typing import Type import numpy as np import matplotlib.pyplot as plt from matplotlib.axes import Axes @@ -521,7 +522,7 @@ def to_pi_fraction(self, value: float, tolerance: float = 0.01) -> str: def _draw_multiq_gate( self, - gate: Gate, + gate: Gate | Type[Gate], targets: list[int], controls: list[int], cbits: list[int], diff --git a/src/qutip_qip/circuit/draw/text_renderer.py b/src/qutip_qip/circuit/draw/text_renderer.py index 55bdabf84..f8387d215 100644 --- a/src/qutip_qip/circuit/draw/text_renderer.py +++ b/src/qutip_qip/circuit/draw/text_renderer.py @@ -3,6 +3,7 @@ """ from math import ceil +from typing import Type from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import BaseRenderer, StyleConfig @@ -117,7 +118,7 @@ def _draw_singleq_gate( def _draw_multiq_gate( self, - gate: Gate, + gate: Gate | Type[Gate], gate_text: str, targets: list[int], controls: list[int], @@ -334,7 +335,7 @@ def _update_target_multiq( def _update_qbridge( self, - gate: Gate, + gate: Gate | Type[Gate], targets: list[int], controls: list[int], wire_list_control: list[int], diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index f3db34a1d..08df01528 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Type from dataclasses import dataclass, field from qutip_qip.operations import Gate, Measurement @@ -17,7 +18,7 @@ def _validate_non_negative_int_tuple(T: any, txt: str = ""): @dataclass(frozen=True, slots=True) class CircuitInstruction(ABC): - operation: Gate | Measurement + operation: Gate | Type[Gate] | Measurement qubits: tuple[int, ...] = tuple() cbits: tuple[int, ...] = tuple() style: dict = field(default_factory=dict) @@ -58,7 +59,7 @@ def __repr__(self) -> str: @dataclass(frozen=True, slots=True) class GateInstruction(CircuitInstruction): - operation: Gate + operation: Gate | Type[Gate] cbits_ctrl_value: int | None = None def __post_init__(self) -> None: diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 0bacfa376..e6b0efb49 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -34,7 +34,7 @@ class ControlledGate(Gate): Attributes ---------- - target_gate : Gate + target_gate : :class:`.Gate` The gate to be applied to the target qubits. num_ctrl_qubits : int @@ -132,7 +132,7 @@ def __setattr__(self, name, value) -> None: # This is because Python currently doesn't support abstract class attributes. @property @abstractmethod - def target_gate() -> Gate: + def target_gate() -> Type[Gate]: pass @classmethod @@ -234,7 +234,7 @@ def __eq__(self, other) -> bool: def controlled( - gate: Gate, + gate: Type[Gate], n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 5793b60c7..be5bf0ea7 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -1,6 +1,5 @@ from functools import cache, lru_cache from typing import Final, Type -from abc import abstractmethod import warnings import numpy as np diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 06682cf3b..5940787f7 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -1,5 +1,4 @@ from typing import Final, Type -from abc import abstractmethod from functools import cache, lru_cache import warnings diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 730f6231d..d3a3fab4d 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -7,11 +7,12 @@ from copy import deepcopy from collections.abc import Iterable from math import pi # Don't remove +from typing import Type import numpy as np from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import get_unitary_gate +from qutip_qip.operations import Gate, get_unitary_gate import qutip_qip.operations.gates as gates __all__ = ["read_qasm", "save_qasm", "print_qasm", "circuit_to_qasm_str"] @@ -1004,7 +1005,7 @@ def _qasm_str(self, q_name, q_targets, q_controls=None, q_args=None): else: return "{} {};".format(q_name, q_regs) - def _qasm_defns(self, gate): + def _qasm_defns(self, gate: Gate | Type[Gate]): """ Define QASM gates for QuTiP gates that do not have QASM counterparts. diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index 1a707f4e4..9928f68a0 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -1,13 +1,14 @@ """Conversion of circuits from qiskit to qutip_qip.""" from collections.abc import Iterable +from typing import Type from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit from qutip_qip.operations import Gate import qutip_qip.operations.gates as gates # TODO Expand this dictionary for other gates like CS etc. -_map_gates: dict[str, Gate] = { +_map_gates: dict[str, Type[Gate]] = { "p": gates.PHASE, "x": gates.X, "y": gates.Y, @@ -22,7 +23,7 @@ "u": gates.QASMU, } -_map_controlled_gates: dict[str, Gate] = { +_map_controlled_gates: dict[str, Type[Gate]] = { "cx": gates.CX, "cy": gates.CY, "cz": gates.CZ, diff --git a/src/qutip_qip/qiskit/utils/target_gate_set.py b/src/qutip_qip/qiskit/utils/target_gate_set.py index 6f3d0b4f8..31c907d87 100644 --- a/src/qutip_qip/qiskit/utils/target_gate_set.py +++ b/src/qutip_qip/qiskit/utils/target_gate_set.py @@ -1,4 +1,4 @@ -from qiskit.circuit.gate import Gate +import qiskit from qiskit.circuit.library import ( PhaseGate, XGate, @@ -21,7 +21,7 @@ CPhaseGate, ) -QUTIP_TO_QISKIT_GATE_MAP: dict[str, Gate] = { +QUTIP_TO_QISKIT_GATE_MAP: dict[str, qiskit.circuit.Gate] = { # Single Qubit Gates "PHASEGATE": PhaseGate(theta=0.0), "X": XGate(), diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 113c55b6f..4f03e3318 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -10,7 +10,7 @@ from scipy.linalg import expm_frechet from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import gate_sequence_product, get_unitary_gate, Gate +from qutip_qip.operations import Gate, gate_sequence_product, get_unitary_gate class VQA: diff --git a/tests/test_gates.py b/tests/test_gates.py index 4194d3cda..f8a168727 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -1,6 +1,7 @@ from copy import deepcopy import pytest import itertools +from typing import Type import numpy as np import qutip from qutip_qip.circuit import QubitCircuit @@ -206,7 +207,7 @@ class TestGateExpansion: pytest.param(gates.R, 2, id="Rabi rotation"), ], ) - def test_single_qubit_rotation(self, gate: Gate, n_angles: int): + def test_single_qubit_rotation(self, gate: Type[Gate], n_angles: int): base = qutip.rand_ket(2) if n_angles > 0: angles = 2 * np.pi * (np.random.rand(n_angles)) @@ -267,7 +268,7 @@ def test_two_qubit(self, gate, n_controls): pytest.param(RandomThreeQubitGate, 2, id="random"), ], ) - def test_three_qubit(self, gate: Gate, n_controls): + def test_three_qubit(self, gate: Type[Gate], n_controls): targets = [qutip.rand_ket(2) for _ in [None] * 3] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(targets) @@ -507,7 +508,7 @@ def test_gates_class(): @pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) -def test_gate_inverse(gate: Gate): +def test_gate_inverse(gate: Gate | Type[Gate]): n = 2**gate.num_qubits inverse = gate.inverse() np.testing.assert_allclose( From b14ed06f95a08a8964701aca38baf67d79282f1a Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 04:40:35 +0530 Subject: [PATCH 072/117] Added new standard gates to the colour theme --- src/qutip_qip/circuit/draw/color_theme.py | 128 ++++++++++++++++++---- 1 file changed, 106 insertions(+), 22 deletions(-) diff --git a/src/qutip_qip/circuit/draw/color_theme.py b/src/qutip_qip/circuit/draw/color_theme.py index df7b3cbd9..d497bdba9 100644 --- a/src/qutip_qip/circuit/draw/color_theme.py +++ b/src/qutip_qip/circuit/draw/color_theme.py @@ -7,29 +7,50 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#000000", # Black - "H": "#6270CE", # Medium Slate Blue - "SNOT": "#6270CE", # Medium Slate Blue + "IDLE": "#FFFFFF", # White "X": "#CB4BF9", # Medium Orchid "Y": "#CB4BF9", # Medium Orchid "Z": "#CB4BF9", # Medium Orchid + "H": "#6270CE", # Medium Slate Blue + "SNOT": "#6270CE", # Medium Slate Blue "S": "#254065", # Dark Slate Blue "T": "#254065", # Dark Slate Blue + "Sdag": "#254065", # Dark Slate Blue + "Tdag": "#254065", # Dark Slate Blue + "SQRTX": "#2CAC70", # Green + "SQRTXdag": "#2CAC70", # Green + "SQRTNOT": "#2CAC70", # Green "RX": "#5EBDF8", # Light Sky Blue "RY": "#5EBDF8", # Light Sky Blue "RZ": "#5EBDF8", # Light Sky Blue - "CPHASE": "#456DB2", # Steel Blue - "TOFFOLI": "#3B3470", # Indigo + "PHASE": "#456DB2", # Steel Blue + "R": "#456DB2", # Steel Blue + "QASMU": "#456DB2", # Steel Blue "SWAP": "#3B3470", # Indigo + "SQRTSWAP": "#3B3470", # Indigo + "SQRTSWAPdag": "#3B3470", # Indigo + "ISWAP": "#3B3470", # Indigo + "ISWAPdag": "#3B3470", # Indigo + "SQRTISWAP": "#3B3470", # Indigo + "SQRTISWAPdag": "#3B3470", # Indigo + "BERKELEY": "#3B3470", # Indigo + "BERKELEYdag": "#3B3470", # Indigo + "SWAPALPHA": "#7648CB", # Dark Orchid + "MS": "#7648CB", # Dark Orchid + "RZX": "#7648CB", # Dark Orchid "CNOT": "#9598F5", # Light Slate Blue "CX": "#9598F5", # Light Slate Blue "CY": "#9598F5", # Light Slate Blue "CZ": "#9598F5", # Light Slate Blue + "CH": "#9598F5", # Light Slate Blue "CS": "#9598F5", # Light Slate Blue "CT": "#9598F5", # Light Slate Blue "CRX": "#A66DDF", # Medium Purple "CRY": "#A66DDF", # Medium Purple "CRZ": "#A66DDF", # Medium Purple - "BERKELEY": "#7648CB", # Dark Orchid + "CPHASE": "#A66DDF", # Medium Purple + "CQASMU": "#A66DDF", # Medium Purple + "TOFFOLI": "#3B3470", # Indigo "FREDKIN": "#7648CB", # Dark Orchid } @@ -38,30 +59,51 @@ "color": "#000000", # Black "wire_color": "#000000", # Black "default_gate": "#D8CDAF", # Bit Dark Beige - "H": "#A3C1DA", # Light Blue - "SNOT": "#A3C1DA", # Light Blue + "IDLE": "#FFFFFF", # White "X": "#F4A7B9", # Light Pink "Y": "#F4A7B9", # Light Pink "Z": "#F4A7B9", # Light Pink + "H": "#A3C1DA", # Light Blue + "SNOT": "#A3C1DA", # Light Blue "S": "#D3E2EE", # Very Light Blue "T": "#D3E2EE", # Very Light Blue + "Sdag": "#D3E2EE", # Very Light Blue + "Tdag": "#D3E2EE", # Very Light Blue + "SQRTX": "#E1E0BA", # Light Yellow + "SQRTXdag": "#E1E0BA", # Light Yellow + "SQRTNOT": "#E1E0BA", # Light Yellow "RX": "#B3E6E4", # Light Teal "RY": "#B3E6E4", # Light Teal "RZ": "#B3E6E4", # Light Teal - "CPHASE": "#D5E0F2", # Light Slate Blue - "TOFFOLI": "#E6CCE6", # Soft Lavender + "PHASE": "#D5E0F2", # Light Slate Blue + "R": "#D5E0F2", # Light Slate Blue + "QASMU": "#D5E0F2", # Light Slate Blue "SWAP": "#FFB6B6", # Lighter Coral Pink + "SQRTSWAP": "#FFB6B6", # Lighter Coral Pink + "SQRTSWAPdag": "#FFB6B6", # Lighter Coral Pink + "ISWAP": "#FFB6B6", # Lighter Coral Pink + "ISWAPdag": "#FFB6B6", # Lighter Coral Pink + "SQRTISWAP": "#FFB6B6", # Lighter Coral Pink + "SQRTISWAPdag": "#FFB6B6", # Lighter Coral Pink + "BERKELEY": "#FFB6B6", # Lighter Coral Pink + "BERKELEYdag": "#FFB6B6", # Lighter Coral Pink + "SWAPALPHA": "#CDC1E8", # Light Purple + "MS": "#CDC1E8", # Light Purple + "RZX": "#CDC1E8", # Light Purple "CNOT": "#E0E2F7", # Very Light Indigo "CX": "#E0E2F7", # Very Light Indigo "CY": "#E0E2F7", # Very Light Indigo "CZ": "#E0E2F7", # Very Light Indigo + "CH": "#E0E2F7", # Very Light Indigo "CS": "#E0E2F7", # Very Light Indigo "CT": "#E0E2F7", # Very Light Indigo "CRX": "#D6C9E8", # Light Muted Purple "CRY": "#D6C9E8", # Light Muted Purple "CRZ": "#D6C9E8", # Light Muted Purple - "BERKELEY": "#CDC1E8", # Light Purple - "FREDKIN": "#CDC1E8", # Light Purple + "CPHASE": "#D6C9E8", # Light Slate Blue + "CQASMU": "#D6C9E8", # Light Slate Blue + "TOFFOLI": "#E6CCE6", # Soft Lavender + "FREDKIN": "#E6CCE6", # Soft Lavender } dark: dict[str, str] = { @@ -69,30 +111,51 @@ "color": "#000000", # Black "wire_color": "#989898", # Dark Gray "default_gate": "#D8BFD8", # (Thistle) - "H": "#AFEEEE", # Pale Turquoise - "SNOT": "#AFEEEE", # Pale Turquoise + "IDLE": "#FFFFFF", # White "X": "#9370DB", # Medium Purple "Y": "#9370DB", # Medium Purple "Z": "#9370DB", # Medium Purple "S": "#B0E0E6", # Powder Blue + "H": "#AFEEEE", # Pale Turquoise + "SNOT": "#AFEEEE", # Pale Turquoise "T": "#B0E0E6", # Powder Blue + "Sdag": "#B0E0E6", # Powder Blue + "Tdag": "#B0E0E6", # Powder Blue + "SQRTX": "#718520", # Olive Yellow + "SQRTXdag": "#718520", # Olive Yellow + "SQRTNOT": "#718520", # Olive Yellow "RX": "#87CEEB", # Sky Blue "RY": "#87CEEB", # Sky Blue "RZ": "#87CEEB", # Sky Blue - "CPHASE": "#8A2BE2", # Blue Violet - "TOFFOLI": "#DA70D6", # Orchid + "PHASE": "#8A2BE2", # Blue Violet + "R": "#8A2BE2", # Blue Violet + "QASMU": "#8A2BE2", # Blue Violet "SWAP": "#BA55D3", # Medium Orchid + "SQRTSWAP": "#BA55D3", # Medium Orchid + "SQRTSWAPdag": "#BA55D3", # Medium Orchid + "ISWAP": "#BA55D3", # Medium Orchid + "ISWAPdag": "#BA55D3", # Medium Orchid + "SQRTISWAP": "#BA55D3", # Medium Orchid + "SQRTISWAPdag": "#BA55D3", # Medium Orchid + "BERKELEY": "#BA55D3", # Medium Orchid + "BERKELEYdag": "#BA55D3", # Medium Orchid + "SWAPALPHA": "#6A5ACD", # Slate Blue + "MS": "#6A5ACD", # Slate Blue + "RZX": "#6A5ACD", # Slate Blue "CNOT": "#4682B4", # Steel Blue "CX": "#4682B4", # Steel Blue "CY": "#4682B4", # Steel Blue "CZ": "#4682B4", # Steel Blue + "CH": "#4682B4", # Steel Blue "CS": "#4682B4", # Steel Blue "CT": "#4682B4", # Steel Blue "CRX": "#7B68EE", # Medium Slate Blue "CRY": "#7B68EE", # Medium Slate Blue "CRZ": "#7B68EE", # Medium Slate Blue - "BERKELEY": "#6A5ACD", # Slate Blue - "FREDKIN": "#6A5ACD", # Slate Blue + "CPHASE": "#DA70D6", # Orchid + "CQASMU": "#DA70D6", # Orchid + "TOFFOLI": "#43414F", # Dark Gray + "FREDKIN": "#43414F", # Dark Gray } @@ -101,28 +164,49 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#ED9455", # Slate Orange - "H": "#C25454", # Soft Red - "SNOT": "#C25454", # Soft Red + "IDLE": "#FFFFFF", # White "X": "#4A5D6D", # Dark Slate Blue "Y": "#4A5D6D", # Dark Slate Blue "Z": "#4A5D6D", # Dark Slate Blue + "H": "#C25454", # Soft Red + "SNOT": "#C25454", # Soft Red "S": "#2C3E50", # Very Dark Slate Blue "T": "#2C3E50", # Very Dark Slate Blue + "Sdag": "#2C3E50", # Very Dark Slate Blue + "Tdag": "#2C3E50", # Very Dark Slate Blue + "SQRTX": "#D2E587", # Yellow + "SQRTXdag": "#D2E587", # Yellow + "SQRTNOT": "#D2E587", # Yellow "RX": "#2F4F4F", # Dark Slate Teal "RY": "#2F4F4F", # Dark Slate Teal "RZ": "#2F4F4F", # Dark Slate Teal - "CPHASE": "#5E7D8B", # Dark Slate Blue - "TOFFOLI": "#4A4A4A", # Dark Gray + "PHASE": "#5E7D8B", # Dark Slate Blue + "R": "#5E7D8B", # Dark Slate Blue + "QASMU": "#5E7D8B", # Dark Slate Blue "SWAP": "#6A9ACD", # Slate Blue + "SQRTSWAP": "#6A9ACD", # Slate Blue + "SQRTSWAPdag": "#6A9ACD", # Slate Blue + "ISWAP": "#6A9ACD", # Slate Blue + "ISWAPdag": "#6A9ACD", # Slate Blue + "SQRTISWAP": "#6A9ACD", # Slate Blue + "SQRTISWAPdag": "#6A9ACD", # Slate Blue + "BERKELEY": "#6A9ACD", # Slate Blue + "BERKELEYdag": "#6A9ACD", # Slate Blue + "SWAPALPHA": "#4A5D6D", # Dark Slate Blue + "MS": "#4A5D6D", # Dark Slate Blue + "RZX": "#4A5D6D", # Dark Slate Blue "CNOT": "#5D8AA8", # Medium Slate Blue "CX": "#5D8AA8", # Medium Slate Blue "CY": "#5D8AA8", # Medium Slate Blue "CZ": "#5D8AA8", # Medium Slate Blue + "CH": "#5D8AA8", # Medium Slate Blue "CS": "#5D8AA8", # Medium Slate Blue "CT": "#5D8AA8", # Medium Slate Blue "CRX": "#6C5B7B", # Dark Lavender "CRY": "#6C5B7B", # Dark Lavender "CRZ": "#6C5B7B", # Dark Lavender - "BERKELEY": "#4A5D6D", # Dark Slate Blue + "CPHASE": "#4A4A4A", # Dark Gray + "CQASMU": "#4A4A4A", # Dark Gray + "TOFFOLI": "#4A5D6D", # Dark Slate Blue "FREDKIN": "#4A5D6D", # Dark Slate Blue } From efa30db4a50c744c1e1bda023427152efc37043a Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 04:55:48 +0530 Subject: [PATCH 073/117] Added new gates to qiskit converter --- src/qutip_qip/qiskit/utils/converter.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index 9928f68a0..bcb9b88ac 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -7,30 +7,39 @@ from qutip_qip.operations import Gate import qutip_qip.operations.gates as gates -# TODO Expand this dictionary for other gates like CS etc. +# TODO Expand this dictionary for all the valid qiskit gates +# https://quantum.cloud.ibm.com/docs/en/api/qiskit/circuit_library#standard-gates _map_gates: dict[str, Type[Gate]] = { - "p": gates.PHASE, "x": gates.X, "y": gates.Y, "z": gates.Z, "h": gates.H, "s": gates.S, + "sdag": gates.Sdag, "t": gates.T, + "tdag": gates.Tdag, + "sx": gates.SQRTX, + "sxdag": gates.SQRTXdag, "rx": gates.RX, "ry": gates.RY, "rz": gates.RZ, + "p": gates.PHASE, + "u3": gates.QASMU, "swap": gates.SWAP, - "u": gates.QASMU, } _map_controlled_gates: dict[str, Type[Gate]] = { "cx": gates.CX, "cy": gates.CY, "cz": gates.CZ, + "ch": gates.CH, + "cs": gates.CS, + "ct": gates.CT, "crx": gates.CRX, "cry": gates.CRY, "crz": gates.CRZ, "cp": gates.CPHASE, + "cu3": gates.CQASMU, } _ignore_gates: list[str] = ["id", "barrier"] From b7449ba61c9c2aed09c05d5e416df1ddfe3ddbec Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 05:09:35 +0530 Subject: [PATCH 074/117] Added new standard gates to the docs --- doc/source/qip-basics.rst | 43 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/doc/source/qip-basics.rst b/doc/source/qip-basics.rst index f3a16fd2d..b95ab81ad 100644 --- a/doc/source/qip-basics.rst +++ b/doc/source/qip-basics.rst @@ -139,38 +139,49 @@ The pre-defined gates for the class :class:`~.operations.Gate` are shown in the ==================== ======================================== Gate name Description ==================== ======================================== -"RX" Rotation around x axis -"RY" Rotation around y axis -"RZ" Rotation around z axis -"R" Arbitrary single qubit rotation "X" Pauli-X gate "Y" Pauli-Y gate "Z" Pauli-Z gate +"H" Hadamard gate "S" Single-qubit rotation or Z90 +"Sdag" Inverse of S gate "T" Square root of S gate +"Tdag" Inverse of T gate "SQRTX" Square root of X gate -"H" Hadamard gate -"PHASEGATE" Add a phase one the state 1 -"CRX" Controlled rotation around x axis -"CRY" Controlled rotation around y axis -"CRZ" Controlled rotation around z axis -"CX" Controlled X gate (also called CNOT) +"SQRTXdag" Inverse of SQRTX gate +"RX" Rotation around x axis +"RY" Rotation around y axis +"RZ" Rotation around z axis +"PHASE" Adds a relative phase to ket 1 +"R" Arbitrary single qubit rotation +"QASMU" U rotation gate used as a primitive in the QASM standard +"CX" (CNOT) Controlled X gate "CY" Controlled Y gate "CZ" Controlled Z gate +"CH" Controlled H gate "CS" Controlled S gate "CT" Controlled T gate -"CPHASE" Controlled phase gate -"QASMU" U rotation gate used as a primitive in the QASM standard -"BERKELEY" Berkeley gate -"SWAPalpha" SWAPalpha gate +"CRX" Controlled rotation around x axis +"CRY" Controlled rotation around y axis +"CRZ" Controlled rotation around z axis +"CPHASE" Controlled Phase gate +"CQASMU" Controlled QASMU gate "SWAP" Swap the states of two qubits "ISWAP" Swap gate with additional phase for 01 and 10 states +"ISWAPdag" Inverse of ISWAP gate "SQRTSWAP" Square root of the SWAP gate +"SQRTSWAPdag" Inverse of SQRTSWAP gate "SQRTISWAP" Square root of the ISWAP gate +"SQRTISWAPdag" Inverse of SQRTISWAP gate +"BERKELEY" Berkeley gate +"BERKELEYdag" Inverse of BERKELEY gate +"SWAPALPHA" SWAPALPHA gate "MS" Mølmer-Sørensen gate +"RZX" RZX gate +"TOFFOLI" (CCX) Toffoli gate "FREDKIN" Fredkin gate -"TOFFOLI" Toffoli gate -"GLOBALPHASE" Global phase +"GLOBALPHASE" Global phase gate +"IDLE" Identity gate ==================== ======================================== For some of the gates listed above, :class:`.QubitCircuit` also has a primitive :func:`.QubitCircuit.resolve_gates()` method that decomposes them into elementary gate sets such as CX or SWAP with single-qubit gates (RX, RY and RZ). However, this method is not fully optimized. It is very likely that the depth of the circuit can be further reduced by merging quantum gates. It is required that the gate resolution be carried out before the measurements to the circuit are added. From c40284d8691b113fde02440ece5b67fb8bf839cc Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 05:30:00 +0530 Subject: [PATCH 075/117] Separated namespace from gates as a broader concept --- src/qutip_qip/operations/namespace.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/qutip_qip/operations/namespace.py diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py new file mode 100644 index 000000000..40301b285 --- /dev/null +++ b/src/qutip_qip/operations/namespace.py @@ -0,0 +1,37 @@ +from __future__ import annotations # To be removed base version is Python 3.14 +from functools import cached_property +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class NameSpace: + local_name: str + parent: NameSpace | None = None + + def __post_init__(self): + if "." in self.local_name: + raise ValueError( + f"Namespace local_name '{self.local_name}' cannot contain dots. " + f"Dots are reserved for hierarchical resolution." + ) + if not self.local_name.isidentifier(): + raise ValueError( + f"'{self.local_name}' is not a valid namespace identifier." + ) + + @property + @cached_property + def name(self) -> str: + if self.parent: + return f"{self.parent.name}.{self.local_name}" + return self.local_name + + def __str__(self) -> str: + return self.name + + +# The Root +DEFAULT = NameSpace("__default__") + +# For standard gates and measurement +STD = NameSpace("std") From e87100db0970b2087a87613d727a56a6d4a01b28 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 05:57:30 +0530 Subject: [PATCH 076/117] Added a NameSpaceRegistry functionality --- src/qutip_qip/namespace.py | 76 +++++++++++++++++++++++++++ src/qutip_qip/operations/namespace.py | 37 ------------- 2 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 src/qutip_qip/namespace.py delete mode 100644 src/qutip_qip/operations/namespace.py diff --git a/src/qutip_qip/namespace.py b/src/qutip_qip/namespace.py new file mode 100644 index 000000000..959fd01bd --- /dev/null +++ b/src/qutip_qip/namespace.py @@ -0,0 +1,76 @@ +# annotations can be removed when base version is Python 3.14 +from __future__ import annotations +from functools import cached_property +from dataclasses import dataclass + +__all__ = ["NameSpace", "NameSpaceRegistry", "STD_NS"] + + +class _SingletonMeta(type): + """ + Note this is not a thread-safe implementation of Singleton. + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + + return cls._instances[cls] + + +class _GlobalRegistry(metaclass=_SingletonMeta): + def __init__(self): + self._registry: dict[NameSpace, dict[str, any]] = {} + + def register(self, namespace: NameSpace, name: str, item: any) -> None: + """Safely adds an item to the specific namespace.""" + if namespace not in self._registry: + self._registry[namespace] = {} + + name_upper = name.upper() + if name_upper in self._registry[namespace]: + raise ValueError( + f"'{name_upper}' already exists in namespace '{namespace}'" + ) + + self._registry[namespace][name_upper] = item + + def get(self, namespace: NameSpace, name: str) -> any: + """Retrieves an item from a specific namespace.""" + try: + return self._registry[namespace][name.upper()] + except KeyError: + raise KeyError( + f"'{name.upper()}' not found in namespace '{namespace}'." + ) + + +@dataclass(frozen=True, slots=True) +class NameSpace: + local_name: str + parent: NameSpace | None = None + + def __post_init__(self): + if "." in self.local_name: + raise ValueError( + f"Namespace local_name '{self.local_name}' cannot contain dots. " + f"Dots are reserved for hierarchical resolution." + ) + + @cached_property + def name(self) -> str: + if self.parent: + return f"{self.parent.name}.{self.local_name}" + return self.local_name + + def __str__(self) -> str: + return self.name + + def __repr__(self): + return str(self) + + +NameSpaceRegistry = _GlobalRegistry() +STD_NS = NameSpace("std") # DEFAULT NAMESPACE diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py deleted file mode 100644 index 40301b285..000000000 --- a/src/qutip_qip/operations/namespace.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations # To be removed base version is Python 3.14 -from functools import cached_property -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True) -class NameSpace: - local_name: str - parent: NameSpace | None = None - - def __post_init__(self): - if "." in self.local_name: - raise ValueError( - f"Namespace local_name '{self.local_name}' cannot contain dots. " - f"Dots are reserved for hierarchical resolution." - ) - if not self.local_name.isidentifier(): - raise ValueError( - f"'{self.local_name}' is not a valid namespace identifier." - ) - - @property - @cached_property - def name(self) -> str: - if self.parent: - return f"{self.parent.name}.{self.local_name}" - return self.local_name - - def __str__(self) -> str: - return self.name - - -# The Root -DEFAULT = NameSpace("__default__") - -# For standard gates and measurement -STD = NameSpace("std") From 7ee0f6354ef83d7b46f21540ff90f2efb5e237b2 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 06:07:15 +0530 Subject: [PATCH 077/117] Use NameSpaceRegistry in Gate Meta class --- src/qutip_qip/operations/gateclass.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 780c3feda..0f8a8f2f8 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -6,6 +6,9 @@ import numpy as np from qutip import Qobj +from qutip_qip.namespace import NameSpace, NameSpaceRegistry, STD_NS + +GATE_NS = NameSpace("gates", parent=STD_NS) class _GateMetaClass(ABCMeta): @@ -41,17 +44,8 @@ def __init__(cls, name, bases, attrs): cls._is_frozen = True return - namespace = attrs.get("namespace", "std") - name = cls.name - - if namespace not in cls._registry: - cls._registry[namespace] = set() - - if name in cls._registry[namespace]: - raise TypeError( - f"Gate Conflict: '{name}' is already defined in namespace '{namespace}' " - ) - cls._registry[namespace].add(name) + namespace = attrs.get("namespace", GATE_NS) + NameSpaceRegistry.register(namespace, cls.name, cls) # This class attribute (flag) signals class (or subclass) is built, # don't overwrite any defaults like num_qubits etc in __setattr__. From b8889c013093131c7fd346e81ad72f3c686c7564 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 18:25:11 +0530 Subject: [PATCH 078/117] Change the NameSpace dataclass Let every namespace represent the operations within it. Also have a global registry to ensure there are no 2 conflicting namespaces. --- src/qutip_qip/operations/__init__.py | 10 ++++ src/qutip_qip/operations/controlled.py | 11 ++-- src/qutip_qip/operations/gateclass.py | 11 ++-- src/qutip_qip/{ => operations}/namespace.py | 59 ++++++++++++--------- 4 files changed, 58 insertions(+), 33 deletions(-) rename src/qutip_qip/{ => operations}/namespace.py (55%) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 46784943c..188f5c0c0 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -2,6 +2,12 @@ Operations on quantum circuits. """ +from .namespace import ( + GlobalNameSpaceRegistry, + NameSpace, + STD_NS, + USER_NS, +) from .utils import ( hadamard_transform, expand_operator, @@ -48,6 +54,10 @@ ) __all__ = [ + "NameSpace", + "GlobalNameSpaceRegistry", + "STD_NS", + "USER_NS", "Gate", "ParametricGate", "ControlledGate", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index e6b0efb49..002338f38 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -4,7 +4,12 @@ from abc import abstractmethod from qutip import Qobj -from qutip_qip.operations import Gate, controlled_gate_unitary +from qutip_qip.operations import ( + Gate, + NameSpace, + USER_NS, + controlled_gate_unitary, +) class class_or_instance_method: @@ -238,7 +243,7 @@ def controlled( n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, - namespace: str = "custom", + gate_namespace: NameSpace = USER_NS, ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. @@ -252,7 +257,7 @@ def controlled( class _CustomControlledGate(ControlledGate): __slots__ = () - _namespace = namespace + namespace = gate_namespace name = gate_name num_qubits = n_ctrl_qubits + gate.num_qubits num_ctrl_qubits = n_ctrl_qubits diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 0f8a8f2f8..05ca418d1 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -6,7 +6,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.namespace import NameSpace, NameSpaceRegistry, STD_NS +from qutip_qip.operations import NameSpace, STD_NS GATE_NS = NameSpace("gates", parent=STD_NS) @@ -44,12 +44,13 @@ def __init__(cls, name, bases, attrs): cls._is_frozen = True return - namespace = attrs.get("namespace", GATE_NS) - NameSpaceRegistry.register(namespace, cls.name, cls) - - # This class attribute (flag) signals class (or subclass) is built, + # _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 = attrs.get("namespace", GATE_NS) + + # We obviously don't register abstract classes to the namespace. + namespace.register(cls.name, cls) def __setattr__(cls, name: str, value: any) -> None: """ diff --git a/src/qutip_qip/namespace.py b/src/qutip_qip/operations/namespace.py similarity index 55% rename from src/qutip_qip/namespace.py rename to src/qutip_qip/operations/namespace.py index 959fd01bd..fcfe85c23 100644 --- a/src/qutip_qip/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -1,9 +1,7 @@ -# annotations can be removed when base version is Python 3.14 +# annotations can be removed when base version is Python 3.14 (PEP 749) from __future__ import annotations from functools import cached_property -from dataclasses import dataclass - -__all__ = ["NameSpace", "NameSpaceRegistry", "STD_NS"] +from dataclasses import dataclass, field class _SingletonMeta(type): @@ -22,35 +20,23 @@ def __call__(cls, *args, **kwargs): class _GlobalRegistry(metaclass=_SingletonMeta): def __init__(self): - self._registry: dict[NameSpace, dict[str, any]] = {} + self._registry: set[NameSpace] = set() - def register(self, namespace: NameSpace, name: str, item: any) -> None: + def register_namespace(self, namespace: NameSpace) -> None: """Safely adds an item to the specific namespace.""" - if namespace not in self._registry: - self._registry[namespace] = {} - - name_upper = name.upper() - if name_upper in self._registry[namespace]: - raise ValueError( - f"'{name_upper}' already exists in namespace '{namespace}'" - ) + if namespace in self._registry: + raise ValueError(f"Existing namespace {namespace}") + self._registry.add(namespace) - self._registry[namespace][name_upper] = item - def get(self, namespace: NameSpace, name: str) -> any: - """Retrieves an item from a specific namespace.""" - try: - return self._registry[namespace][name.upper()] - except KeyError: - raise KeyError( - f"'{name.upper()}' not found in namespace '{namespace}'." - ) +GlobalNameSpaceRegistry = _GlobalRegistry() -@dataclass(frozen=True, slots=True) +@dataclass class NameSpace: local_name: str parent: NameSpace | None = None + _registry: dict[str, any] = field(default_factory=dict) def __post_init__(self): if "." in self.local_name: @@ -58,6 +44,7 @@ def __post_init__(self): f"Namespace local_name '{self.local_name}' cannot contain dots. " f"Dots are reserved for hierarchical resolution." ) + GlobalNameSpaceRegistry.register_namespace(self) @cached_property def name(self) -> str: @@ -65,6 +52,28 @@ def name(self) -> str: return f"{self.parent.name}.{self.local_name}" return self.local_name + def register(self, name: str, item: any) -> None: + """Safely adds an item to the specific namespace.""" + name_upper = name.upper() + if name_upper in self._registry: + raise ValueError( + f"'{name_upper}' already exists in namespace '{self.name}'" + ) + + self._registry[name_upper] = item + + def get(self, name: str) -> any: + """Retrieves an item from a specific namespace.""" + try: + return self._registry[name.upper()] + except KeyError: + raise KeyError( + f"'{name.upper()}' not found in namespace '{self.name}'." + ) + + def __hash__(self) -> int: + return hash(self.name) + def __str__(self) -> str: return self.name @@ -72,5 +81,5 @@ def __repr__(self): return str(self) -NameSpaceRegistry = _GlobalRegistry() STD_NS = NameSpace("std") # DEFAULT NAMESPACE +USER_NS = NameSpace("user") # Default for anything defined by the user From c37ac573aec5a500466f133c31ba3d9c1b8c7a20 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 18:48:24 +0530 Subject: [PATCH 079/117] Replace .format with f-strings --- doc/pulse-paper/qft.py | 6 +++--- src/qutip_qip/circuit/circuit.py | 4 ++-- src/qutip_qip/circuit/draw/texrenderer.py | 2 +- .../decompose/decompose_single_qubit_gate.py | 2 +- src/qutip_qip/device/optpulseprocessor.py | 16 +++++++-------- src/qutip_qip/device/processor.py | 20 +++++++++---------- src/qutip_qip/device/spinchain.py | 10 ++++------ src/qutip_qip/noise/relaxation.py | 10 +++++----- src/qutip_qip/operations/utils.py | 2 +- src/qutip_qip/pulse/evo_element.py | 4 +--- src/qutip_qip/pulse/pulse.py | 6 ++---- src/qutip_qip/qasm.py | 20 ++++++++----------- tests/conftest.py | 4 ++-- tests/test_processor.py | 2 +- 14 files changed, 48 insertions(+), 60 deletions(-) diff --git a/doc/pulse-paper/qft.py b/doc/pulse-paper/qft.py index 7014a018a..bcd5b4a5e 100644 --- a/doc/pulse-paper/qft.py +++ b/doc/pulse-paper/qft.py @@ -42,9 +42,9 @@ def get_control_latex(model): num_qubits = model.num_qubits num_coupling = model._get_num_coupling() return [ - {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)}, - {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)}, - {f"g{m}": r"$g_{}$".format(m) for m in range(num_coupling)}, + {f"sx{m}": rf"$\sigma_x^{m}$" for m in range(num_qubits)}, + {f"sz{m}": rf"$\sigma_z^{m}$" for m in range(num_qubits)}, + {f"g{m}": rf"$g_{m}$" for m in range(num_coupling)}, ] diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 471b67490..c0f4b3423 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -946,9 +946,9 @@ def _to_qasm(self, qasm_out): object to store QASM output. """ - qasm_out.output("qreg q[{}];".format(self.num_qubits)) + qasm_out.output(f"qreg q[{self.num_qubits}];") if self.num_cbits: - qasm_out.output("creg c[{}];".format(self.num_cbits)) + qasm_out.output(f"creg c[{self.num_cbits}];") qasm_out.output(n=1) for circ_instruction in self.instructions: diff --git a/src/qutip_qip/circuit/draw/texrenderer.py b/src/qutip_qip/circuit/draw/texrenderer.py index 378089e87..9180ef050 100644 --- a/src/qutip_qip/circuit/draw/texrenderer.py +++ b/src/qutip_qip/circuit/draw/texrenderer.py @@ -263,7 +263,7 @@ def _convert_pdf(file_stem: str, dpi: int | None = None) -> bytes: @classmethod def _make_converter( self, configuration: dict - ) -> Callable[dict, str | bytes]: + ) -> Callable[[str, int], str | bytes]: """ Create the actual conversion function of signature file_stem: str -> 'T, diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index 18b454277..b2c599e0e 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -93,7 +93,7 @@ def _ZXZ_rotation(input_gate): Phase_gate = GLOBALPHASE(global_phase_angle) Rz_alpha = RZ( alpha, - arg_label=rf"{(alpha / np.pi):0.2f} \times \pi".format, + arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) Rx_theta = RX( theta, diff --git a/src/qutip_qip/device/optpulseprocessor.py b/src/qutip_qip/device/optpulseprocessor.py index 26a6e9ac8..d24fa5970 100644 --- a/src/qutip_qip/device/optpulseprocessor.py +++ b/src/qutip_qip/device/optpulseprocessor.py @@ -201,9 +201,9 @@ def load_circuit( if result.fid_err > min_fid_err: warnings.warn( - "The fidelity error of gate {} is higher " + f"The fidelity error of gate {prop_ind} is higher " "than required limit. Use verbose=True to see" - "the more detailed information.".format(prop_ind) + "the more detailed information." ) time_record.append(result.time[1:] + last_time) @@ -211,13 +211,11 @@ def load_circuit( coeff_record.append(result.final_amps.T) if verbose: - print("********** Gate {} **********".format(prop_ind)) - print("Final fidelity error {}".format(result.fid_err)) - print( - "Final gradient normal {}".format(result.grad_norm_final) - ) - print("Terminated due to {}".format(result.termination_reason)) - print("Number of iterations {}".format(result.num_iter)) + print(f"********** Gate {prop_ind} **********") + print(f"Final fidelity error {result.fid_err}") + print(f"Final gradient normal {result.grad_norm_final}") + print(f"Terminated due to {result.termination_reason}") + print(f"Number of iterations {result.num_iter}") tlist = np.hstack([[0.0]] + time_record) for i in range(len(self.pulses)): diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 66b952b5d..fde81541d 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -655,13 +655,13 @@ def _is_pulses_valid(self): continue if pulse.tlist is None: raise ValueError( - "Pulse id={} is invalid. " - "Please define a tlist for the pulse.".format(i) + f"Pulse id={i} is invalid. " + "Please define a tlist for the pulse." ) if pulse.tlist is not None and pulse.coeff is None: raise ValueError( - "Pulse id={} is invalid. " - "Please define a coeff for the pulse.".format(i) + f"Pulse id={i} is invalid. " + "Please define a coeff for the pulse." ) coeff_len = len(pulse.coeff) tlist_len = len(pulse.tlist) @@ -671,10 +671,10 @@ def _is_pulses_valid(self): else: raise ValueError( "The length of tlist and coeff of the pulse " - "labelled {} is invalid. " + f"labelled {i} is invalid. " "It's either len(tlist)=len(coeff) or " "len(tlist)-1=len(coeff) for coefficients " - "as step function".format(i) + "as step function" ) else: if coeff_len == tlist_len: @@ -682,8 +682,8 @@ def _is_pulses_valid(self): else: raise ValueError( "The length of tlist and coeff of the pulse " - "labelled {} is invalid. " - "It should be either len(tlist)=len(coeff)".format(i) + f"labelled {i} is invalid. " + "It should be either len(tlist)=len(coeff)" ) return True @@ -703,9 +703,9 @@ def find_pulse(self, pulse_name): return self.pulses[pulse_dict[pulse_name]] except KeyError: raise KeyError( - "Pulse name {} undefined. " + f"Pulse name {pulse_name} undefined. " "Please define it in the attribute " - "`pulse_dict`.".format(pulse_name) + "`pulse_dict`." ) @property diff --git a/src/qutip_qip/device/spinchain.py b/src/qutip_qip/device/spinchain.py index 38718c5ef..a6a470d36 100644 --- a/src/qutip_qip/device/spinchain.py +++ b/src/qutip_qip/device/spinchain.py @@ -407,13 +407,11 @@ def get_control_latex(self): num_qubits = self.num_qubits num_coupling = self._get_num_coupling() return [ - {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)}, - {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)}, + {f"sx{m}": rf"$\sigma_x^{m}$" for m in range(num_qubits)}, + {f"sz{m}": rf"$\sigma_z^{m}$" for m in range(num_qubits)}, { - f"g{m}": r"$\sigma_x^{}\sigma_x^{} +" - r" \sigma_y^{}\sigma_y^{}$".format( - m, (m + 1) % num_qubits, m, (m + 1) % num_qubits - ) + f"g{m}": rf"$\sigma_x^{m}\sigma_x^{(m + 1) % num_qubits} +" + rf" \sigma_y^{m}\sigma_y^{(m + 1) % num_qubits}$" for m in range(num_coupling) }, ] diff --git a/src/qutip_qip/noise/relaxation.py b/src/qutip_qip/noise/relaxation.py index 72726bd68..feab463cc 100644 --- a/src/qutip_qip/noise/relaxation.py +++ b/src/qutip_qip/noise/relaxation.py @@ -67,9 +67,9 @@ def _T_to_list(self, T: float | list[float], N: int) -> list[float]: return T else: raise ValueError( - "Invalid relaxation time T={}," + f"Invalid relaxation time T={T}," "either the length is not equal to the number of qubits, " - "or T is not a positive number.".format(T) + "or T is not a positive number." ) def get_noisy_pulses( @@ -108,7 +108,7 @@ def get_noisy_pulses( if len(self.t1) != N or len(self.t2) != N: raise ValueError( "Length of t1 or t2 does not match N, " - "len(t1)={}, len(t2)={}".format(len(self.t1), len(self.t2)) + f"len(t1)={len(self.t1)}, len(t2)={len(self.t2)}" ) if self.targets is None: @@ -129,8 +129,8 @@ def get_noisy_pulses( if t1 is not None: if 2 * t1 < t2: raise ValueError( - "t1={}, t2={} does not fulfill " - "2*t1>t2".format(t1, t2) + f"t1={t1}, t2={t2} does not fulfill " + "2*t1>t2" ) T2_eff = 1.0 / (1.0 / t2 - 1.0 / 2.0 / t1) else: diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index c8cbf89d7..eb9ca7e0e 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -82,7 +82,7 @@ def _targets_to_list( # If targets is smaller than N if N is not None: if not all([t < N for t in targets]): - raise ValueError("Targets must be smaller than N={}.".format(N)) + raise ValueError(f"Targets must be smaller than N={N}.") return targets diff --git a/src/qutip_qip/pulse/evo_element.py b/src/qutip_qip/pulse/evo_element.py index 395d04f50..fd21ceef2 100644 --- a/src/qutip_qip/pulse/evo_element.py +++ b/src/qutip_qip/pulse/evo_element.py @@ -134,9 +134,7 @@ def get_qobjevo( try: return self._get_qobjevo_helper(spline_kind, dims=dims) except Exception as err: - print( - "The Evolution element went wrong was\n {}".format(str(self)) - ) + print(f"The Evolution element went wrong was\n {str(self)}") raise (err) def __str__(self) -> str: diff --git a/src/qutip_qip/pulse/pulse.py b/src/qutip_qip/pulse/pulse.py index 75b39d859..f78d18b3e 100644 --- a/src/qutip_qip/pulse/pulse.py +++ b/src/qutip_qip/pulse/pulse.py @@ -338,10 +338,8 @@ def print_info(self): if self.label is not None: print("Pulse label:", self.label) print( - "The pulse contains: {} coherent noise elements and {} " - "Lindblad noise elements.".format( - len(self.coherent_noise), len(self.lindblad_noise) - ) + f"The pulse contains: {len(self.coherent_noise)} coherent noise " + f"elements and {len(self.lindblad_noise)} Lindblad noise element." ) print() print("Ideal pulse:") diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index d3a3fab4d..70a224697 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -56,9 +56,7 @@ def _tokenize_line(command): if groups: tokens = ["if", "(", groups.group(1), ")"] tokens_gate = _tokenize_line( - "{} ({}) {}".format( - groups.group(2), groups.group(3), groups.group(4) - ) + f"{groups.group(2)} ({groups.group(3)}) {groups.group(4)}" ) tokens += tokens_gate # for classically controlled gates without arguments @@ -770,9 +768,9 @@ def _gate_add( reg_set = self._regs_processor(regs, "gate") if args: - gate_name = "{}({})".format(command[0], ",".join(args)) + gate_name = f"{command[0]}({','.join(args)})" else: - gate_name = "{}".format(command[0]) + gate_name = f"{command[0]}" # creates custom-gate (if required) using gate defn and provided args custom_gate_unitary = None @@ -849,9 +847,7 @@ def _final_pass(self, qc): classical_control_value, ) else: - err = "QASM: {} is not a valid QASM command.".format( - command[0] - ) + err = f"QASM: {command[0]} is not a valid QASM command." raise SyntaxError(err) @@ -994,16 +990,16 @@ def _qasm_str(self, q_name, q_targets, q_controls=None, q_args=None): q_regs = q_controls + q_targets if isinstance(q_targets[0], int): - q_regs = ",".join(["q[{}]".format(reg) for reg in q_regs]) + q_regs = ",".join([f"q[{reg}]" for reg in q_regs]) else: q_regs = ",".join(q_regs) if q_args: if isinstance(q_args, Iterable): q_args = ",".join([str(arg) for arg in q_args]) - return "{}({}) {};".format(q_name, q_args, q_regs) + return f"{q_name}({q_args}) {q_regs};" else: - return "{} {};".format(q_name, q_regs) + return f"{q_name} {q_regs};" def _qasm_defns(self, gate: Gate | Type[Gate]): """ @@ -1145,4 +1141,4 @@ def save_qasm(qc, file_loc): lines = qasm_out._qasm_output(qc) with open(file_loc, "w") as f: for line in lines: - f.write("{}\n".format(line)) + f.write(f"{line}\n") diff --git a/tests/conftest.py b/tests/conftest.py index 0989992d1..8b572ac87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ def _add_repeats_if_marked(metafunc): metafunc.parametrize( "_repeat_count", range(count), - ids=["rep({})".format(x + 1) for x in range(count)], + ids=[f"rep({x + 1})" for x in range(count)], ) @@ -103,7 +103,7 @@ def _patched_build_err_msg( with np.printoptions(threshold=np.inf): r = r_func(a) except Exception as exc: - r = "[repr failed for <{}>: {}]".format(type(a).__name__, exc) + r = f"[repr failed for <{type(a).__name__}>: {exc}]" # [diff] The original truncates the output to 3 lines here. msg.append(" %s: %s" % (names[i], r)) return "\n".join(msg) diff --git a/tests/test_processor.py b/tests/test_processor.py index 82c1cef4e..e570f7fb2 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -153,7 +153,7 @@ def test_id_with_T1_T2(self): np.exp(-1.0 / t2 * end_time) * 0.5 + 0.5, rtol=1e-5, err_msg="Error in t1 & t2 simulation, " - "with t1={} and t2={}".format(t1, t2), + f"with t1={t1} and t2={t2}", ) def test_plot(self): From 5f2e5c8e03deba770b3d62b09039a94bec4a1b53 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 4 Mar 2026 19:10:51 +0530 Subject: [PATCH 080/117] Correct typing and added circuit.utils method to utils.py --- src/qutip_qip/circuit/circuit.py | 16 +++++----- src/qutip_qip/circuit/utils.py | 46 --------------------------- src/qutip_qip/noise/relaxation.py | 19 +++++------ src/qutip_qip/qasm.py | 4 +-- src/qutip_qip/utils.py | 53 ++++++++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 66 deletions(-) delete mode 100644 src/qutip_qip/circuit/utils.py diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index c0f4b3423..2d4472ab5 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -14,10 +14,10 @@ GateInstruction, MeasurementInstruction, ) -from qutip_qip.circuit.utils import check_limit, convert_int_to_list from qutip_qip.operations import Gate, Measurement, expand_operator from qutip_qip.operations import gates as std from qutip_qip.typing import Int, IntSequence +from qutip_qip.utils import check_limit, convert_type_input_to_sequence try: from IPython.display import Image as DisplayImage, SVG as DisplaySVG @@ -240,9 +240,9 @@ def add_measurement( def add_gate( self, gate: Gate | Type[Gate] | str, - targets: int | Iterable[int] = (), - controls: int | Iterable[int] = (), - classical_controls: int | Iterable[int] = (), + targets: int | IntSequence = (), + controls: int | IntSequence = (), + classical_controls: int | IntSequence = (), classical_control_value: int | None = None, style: dict = None, arg_value: None = None, @@ -304,10 +304,10 @@ def add_gate( return # Handling case for integer input - targets = convert_int_to_list("targets", targets) - controls = convert_int_to_list("controls", controls) - classical_controls = convert_int_to_list( - "classical_controls", classical_controls + targets = convert_type_input_to_sequence(int, "targets", targets) + controls = convert_type_input_to_sequence(int, "controls", controls) + classical_controls = convert_type_input_to_sequence( + int, "classical_controls", classical_controls ) # Checks each element within the limit diff --git a/src/qutip_qip/circuit/utils.py b/src/qutip_qip/circuit/utils.py deleted file mode 100644 index ffa2d2da8..000000000 --- a/src/qutip_qip/circuit/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import TypeVar, Sequence -import numpy as np -from qutip_qip.typing import Int, IntSequence - -T = TypeVar("T") # T can be any type - - -def check_limit( - input_name: str, input_value: Sequence[T], lower_limit: T, upper_limit: T -): - if len(input_value) == 0: - return - - min_element = min(input_value) - if min_element < lower_limit: - raise ValueError( - f"Each entry of {input_name} must be greater than {lower_limit}, but found {min_element}." - ) - - max_element = max(input_value) - if max_element > upper_limit: - raise ValueError( - f"Each entry of {input_name} must be less than {upper_limit}, but found {max_element}." - ) - - -def convert_int_to_list( - input_name: str, input_value: Int | IntSequence -) -> IntSequence: - if isinstance(input_value, Int): - return [input_value] - - elif isinstance(input_value, Sequence) and not isinstance( - input_value, str - ): - for i, val in enumerate(input_value): - if not isinstance(val, Int): - raise TypeError( - f"All elements in '{input_name}' must be integers. " - f"Found {type(val).__name__} ({val}) at index {i}." - ) - return input_value - else: - raise TypeError( - f"{input_name} must be an int or sequence of int, got {input_value}." - ) diff --git a/src/qutip_qip/noise/relaxation.py b/src/qutip_qip/noise/relaxation.py index feab463cc..bbf14b13d 100644 --- a/src/qutip_qip/noise/relaxation.py +++ b/src/qutip_qip/noise/relaxation.py @@ -1,10 +1,10 @@ -import numbers -import numpy as np from collections.abc import Iterable +import numpy as np from qutip import destroy, num from qutip_qip.noise import Noise from qutip_qip.pulse import Pulse +from qutip_qip.typing import Int, IntSequence, Real, RealSequence class RelaxationNoise(Noise): @@ -37,15 +37,16 @@ class RelaxationNoise(Noise): def __init__( self, - t1: float | list[float] | None = None, - t2: float | list[float] | None = None, - targets: int | list[int] | None = None, + t1: Real | RealSequence | None = None, + t2: Real | RealSequence | None = None, + targets: Int | IntSequence | None = None, ): self.t1 = t1 self.t2 = t2 self.targets = targets - def _T_to_list(self, T: float | list[float], N: int) -> list[float]: + @staticmethod + def _T_to_list(T: Real | RealSequence, N: int) -> RealSequence: """ Check if the relaxation time is valid @@ -61,7 +62,7 @@ def _T_to_list(self, T: float | list[float], N: int) -> list[float]: T: list of float The relaxation time in Python list form """ - if (isinstance(T, numbers.Real) and T > 0) or T is None: + if (isinstance(T, Real) and T > 0) or T is None: return [T] * N elif isinstance(T, Iterable) and len(T) == N: return T @@ -74,8 +75,8 @@ def _T_to_list(self, T: float | list[float], N: int) -> list[float]: def get_noisy_pulses( self, - dims: list[int] | None = None, - pulses: list[Pulse] | None = None, + dims: IntSequence | None = None, + pulses: RealSequence | None = None, systematic_noise: Pulse | None = None, ) -> tuple[list[Pulse], Pulse]: """ diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 70a224697..60e069115 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -5,7 +5,7 @@ import warnings from itertools import chain from copy import deepcopy -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from math import pi # Don't remove from typing import Type @@ -796,7 +796,7 @@ def _gate_add( classical_control_value=classical_control_value, ) else: - if not isinstance(regs, Iterable): + if not isinstance(regs, Sequence): regs = [regs] if custom_gate_unitary is not None: diff --git a/src/qutip_qip/utils.py b/src/qutip_qip/utils.py index 45ac57109..cab031421 100644 --- a/src/qutip_qip/utils.py +++ b/src/qutip_qip/utils.py @@ -2,9 +2,16 @@ Module for Helper functions. """ +from typing import TypeVar, Sequence from qutip import Qobj -__all__ = ["valid_unitary"] +__all__ = [ + "valid_unitary", + "check_limit", + "convert_type_input_to_sequence", +] + +T = TypeVar("T") # T can be any type def valid_unitary(gate, num_qubits): @@ -31,3 +38,47 @@ def valid_unitary(gate, num_qubits): if gate.dims != [[2] * num_qubits] * 2: raise ValueError(f"Input is not a unitary on {num_qubits} qubits.") + + +def check_limit( + input_name: str, input_value: Sequence[T], lower_limit: T, upper_limit: T +): + if len(input_value) == 0: + return + + min_element = min(input_value) + if min_element < lower_limit: + raise ValueError( + f"Each entry of {input_name} must be greater than {lower_limit}, but found {min_element}." + ) + + max_element = max(input_value) + if max_element > upper_limit: + raise ValueError( + f"Each entry of {input_name} must be less than {upper_limit}, but found {max_element}." + ) + + +def convert_type_input_to_sequence( + input_type: T, + input_name: str, + input_value: T | Sequence[T], +) -> Sequence[T]: + if isinstance(input_value, input_type): + return [input_value] + + elif isinstance(input_value, Sequence) and not isinstance( + input_value, str + ): + for i, val in enumerate(input_value): + if not isinstance(val, input_type): + raise TypeError( + f"All elements in '{input_name}' must be {input_type}. " + f"Found {type(val).__name__} ({val}) at index {i}." + ) + return input_value + + else: + raise TypeError( + f"{input_name} must be an {input_type} or sequence of {input_type}, got {input_value}." + ) From 03a483d2d3d914c9eda2742aab93e84092ccd435 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Thu, 5 Mar 2026 02:35:53 +0530 Subject: [PATCH 081/117] Corrected some minor typehinting as per Python 3.10 standards --- src/qutip_qip/device/model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qutip_qip/device/model.py b/src/qutip_qip/device/model.py index 408a3c50e..c56c7093c 100644 --- a/src/qutip_qip/device/model.py +++ b/src/qutip_qip/device/model.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import List, Tuple, Hashable +from typing import Hashable from qutip import Qobj from qutip_qip.noise import Noise @@ -40,7 +40,7 @@ def __init__(self, num_qubits, dims=None, **params): self._noise = [] # TODO make this a property - def get_all_drift(self) -> List[Tuple[Qobj, List[int]]]: + def get_all_drift(self) -> list[tuple[Qobj, list[int]]]: """ Get all the drift Hamiltonians. @@ -52,7 +52,7 @@ def get_all_drift(self) -> List[Tuple[Qobj, List[int]]]: """ return self._drift - def get_control(self, label: Hashable) -> Tuple[Qobj, List[int]]: + def get_control(self, label: Hashable) -> tuple[Qobj, list[int]]: """ Get the control Hamiltonian corresponding to the label. @@ -71,7 +71,7 @@ def get_control(self, label: Hashable) -> Tuple[Qobj, List[int]]: label = self._old_index_label_map[label] return self._controls[label] - def get_control_labels(self) -> List[Hashable]: + def get_control_labels(self) -> list[Hashable]: """ Get a list of all available control Hamiltonians. Optional, required only when plotting the pulses or @@ -85,7 +85,7 @@ def get_control_labels(self) -> List[Hashable]: """ return list(self._controls.keys()) - def get_noise(self) -> List[Noise]: + def get_noise(self) -> list[Noise]: """ Get a list of :obj:`.Noise` objects. Single qubit relaxation (T1, T2) are not included here. From 67c2595d862315dd2abcdca4f4c5b72c6ab4af18 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Thu, 5 Mar 2026 04:03:02 +0530 Subject: [PATCH 082/117] Added _namespace to all the standard gates and gate factory functions --- src/qutip_qip/noise/relaxation.py | 3 +-- src/qutip_qip/operations/__init__.py | 6 ++--- src/qutip_qip/operations/controlled.py | 8 ++++--- src/qutip_qip/operations/gateclass.py | 15 +++++-------- src/qutip_qip/operations/gates/other_gates.py | 4 ++++ .../operations/gates/single_qubit_gate.py | 2 ++ .../operations/gates/two_qubit_gate.py | 5 +++++ src/qutip_qip/operations/namespace.py | 5 +++-- tests/test_circuit.py | 22 +++++++++++++------ 9 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/qutip_qip/noise/relaxation.py b/src/qutip_qip/noise/relaxation.py index bbf14b13d..e0ec200e2 100644 --- a/src/qutip_qip/noise/relaxation.py +++ b/src/qutip_qip/noise/relaxation.py @@ -130,8 +130,7 @@ def get_noisy_pulses( if t1 is not None: if 2 * t1 < t2: raise ValueError( - f"t1={t1}, t2={t2} does not fulfill " - "2*t1>t2" + f"t1={t1}, t2={t2} does not fulfill " "2*t1>t2" ) T2_eff = 1.0 / (1.0 / t2 - 1.0 / 2.0 / t1) else: diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 188f5c0c0..c2c4ca959 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -5,8 +5,7 @@ from .namespace import ( GlobalNameSpaceRegistry, NameSpace, - STD_NS, - USER_NS, + NS_USER, ) from .utils import ( hadamard_transform, @@ -56,8 +55,7 @@ __all__ = [ "NameSpace", "GlobalNameSpaceRegistry", - "STD_NS", - "USER_NS", + "NS_USER", "Gate", "ParametricGate", "ControlledGate", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 002338f38..c6446daeb 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -7,7 +7,7 @@ from qutip_qip.operations import ( Gate, NameSpace, - USER_NS, + NS_USER, controlled_gate_unitary, ) @@ -243,14 +243,16 @@ def controlled( n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, - gate_namespace: NameSpace = USER_NS, + gate_namespace: NameSpace = NS_USER, ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ if gate_name is None: - gate_name = f"C{gate.name}" + # print(n_ctrl_qubits) + # gate_name = f"{'C'*{n_ctrl_qubits}}{gate.name}" + gate_name = f"{'C'}{gate.name}" if control_value is None: control_value = 2**n_ctrl_qubits - 1 diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 05ca418d1..d47235e54 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -6,9 +6,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations import NameSpace, STD_NS - -GATE_NS = NameSpace("gates", parent=STD_NS) +from qutip_qip.operations.namespace import NameSpace, NS_USER class _GateMetaClass(ABCMeta): @@ -47,9 +45,9 @@ def __init__(cls, name, bases, attrs): # _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 = attrs.get("namespace", GATE_NS) # We obviously don't register abstract classes to the namespace. + namespace = cls._namespace namespace.register(cls.name, cls) def __setattr__(cls, name: str, value: any) -> None: @@ -163,14 +161,13 @@ class attribute for subclasses. # 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 - _namespace: str = "std" + name: str num_qubits: int self_inverse: bool = False is_clifford: bool = False - - name = None - latex_str = None + latex_str: str def __init_subclass__(cls, **kwargs): """ @@ -279,7 +276,7 @@ def is_parametric() -> bool: def get_unitary_gate( - gate_name: str, U: Qobj, namespace: str = "custom" + gate_name: str, U: Qobj, namespace: NameSpace = NS_USER ) -> Type[Gate]: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 742549daa..b13405799 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -7,6 +7,7 @@ from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate from qutip_qip.operations.gates import X, SWAP +from qutip_qip.operations.namespace import NS_GATE class GLOBALPHASE(AngleParametricGate): @@ -19,6 +20,7 @@ class GLOBALPHASE(AngleParametricGate): """ __slots__ = "phase" + _namespace = NS_GATE num_qubits: Final[int] = 0 num_params: Final[int] = 1 @@ -67,6 +69,7 @@ class TOFFOLI(ControlledGate): """ __slots__ = () + _namespace = NS_GATE num_qubits: Final[int] = 3 num_ctrl_qubits: Final[int] = 2 @@ -115,6 +118,7 @@ class FREDKIN(ControlledGate): """ __slots__ = () + _namespace = NS_GATE num_qubits: Final[int] = 3 num_ctrl_qubits: Final[int] = 1 diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index be5bf0ea7..16432ed54 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -5,12 +5,14 @@ from qutip import Qobj, sigmax, sigmay, sigmaz, qeye from qutip_qip.operations import Gate, AngleParametricGate +from qutip_qip.operations.namespace import NS_GATE class _SingleQubitGate(Gate): """Abstract one-qubit gate.""" __slots__ = () + _namespace = NS_GATE num_qubits: Final[int] = 1 diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 5940787f7..3185b3b75 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -19,12 +19,14 @@ QASMU, PHASE, ) +from qutip_qip.operations.namespace import NS_GATE class _TwoQubitGate(Gate): """Abstract two-qubit gate.""" __slots__ = () + _namespace = NS_GATE num_qubits: Final[int] = 2 @@ -32,6 +34,7 @@ class _TwoQubitParametricGate(AngleParametricGate): """Abstract two-qubit Parametric Gate (non-controlled).""" __slots__ = () + _namespace = NS_GATE num_qubits: Final[int] = 2 @@ -39,6 +42,8 @@ class _ControlledTwoQubitGate(ControlledGate): """Abstract two-qubit Controlled Gate (both parametric and non-parametric).""" __slots__ = () + _namespace = NS_GATE + num_qubits: Final[int] = 2 num_ctrl_qubits: Final[int] = 1 ctrl_value: Final[int] = 1 diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index fcfe85c23..7863267e4 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -81,5 +81,6 @@ def __repr__(self): return str(self) -STD_NS = NameSpace("std") # DEFAULT NAMESPACE -USER_NS = NameSpace("user") # Default for anything defined by the user +NS_STD = NameSpace("std") # DEFAULT NAMESPACE +NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace +NS_USER = NameSpace("user") # Default for anything defined by the user diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 6522fed42..97228a27b 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -5,8 +5,7 @@ from pathlib import Path -from qutip_qip.circuit import QubitCircuit, CircuitSimulator -from qutip_qip.circuit.draw import TeXRenderer +import qutip as qp from qutip import ( tensor, Qobj, @@ -18,13 +17,19 @@ ket2dm, identity, ) -from qutip_qip.qasm import read_qasm -from qutip_qip.operations import Gate, Measurement, gate_sequence_product + +from qutip_qip.circuit import QubitCircuit, CircuitSimulator +from qutip_qip.circuit.draw import TeXRenderer +from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation +from qutip_qip.operations import ( + Gate, + Measurement, + gate_sequence_product, + NS_USER, +) import qutip_qip.operations.gates as gates from qutip_qip.transpiler import to_chain_structure -from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation - -import qutip as qp +from qutip_qip.qasm import read_qasm def _op_dist(A, B): @@ -162,6 +167,7 @@ def test_add_gate(self): assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): + _namespace = NS_USER num_qubits = 1 self_inverse = False @@ -415,6 +421,7 @@ def customer_gate1(arg_values): return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): + _namespace = NS_USER num_qubits = 1 self_inverse = True @@ -442,6 +449,7 @@ def test_N_level_system(self): mat3 = qp.rand_unitary(3) class CTRLMAT3(Gate): + _namespace = NS_USER num_qubits = 2 self_inverse = False From d6814c1bffce037a7aa446cb568ea317648ec1c9 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Thu, 5 Mar 2026 04:35:04 +0530 Subject: [PATCH 083/117] NameSpace functionality working --- src/qutip_qip/algorithms/qpe.py | 2 +- src/qutip_qip/operations/gateclass.py | 43 +++++++++++-------- src/qutip_qip/operations/gates/other_gates.py | 6 +-- .../operations/gates/single_qubit_gate.py | 5 ++- .../operations/gates/two_qubit_gate.py | 6 +-- src/qutip_qip/operations/namespace.py | 13 ++++++ src/qutip_qip/vqa.py | 2 +- tests/test_circuit.py | 6 +-- tests/test_compiler.py | 4 +- 9 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index fe6c1ecca..3de07620b 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -66,7 +66,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): controlled_u = controlled( gate=get_unitary_gate( gate_name=f"U^{power}", - namespace="qpe", + # namespace="qpe", U=U_power, ), ) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index d47235e54..874c5d68a 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -8,21 +8,23 @@ from qutip import Qobj from qutip_qip.operations.namespace import NameSpace, NS_USER +_read_only_set: set[str] = set( + ( + "namespace", + "num_qubits", + "num_ctrl_qubits", + "num_params", + "ctrl_value", + "self_inverse", + "is_clifford", + "target_gate", + "latex_str", + ) +) + class _GateMetaClass(ABCMeta): _registry: dict[str, set] = {} - _read_only_set: set[str] = set( - ( - "num_qubits", - "num_ctrl_qubits", - "num_params", - "ctrl_value", - "self_inverse", - "is_clifford", - "target_gate", - "latex_str", - ) - ) def __init__(cls, name, bases, attrs): """ @@ -47,7 +49,7 @@ def __init__(cls, name, bases, attrs): cls._is_frozen = True # We obviously don't register abstract classes to the namespace. - namespace = cls._namespace + namespace = cls.namespace namespace.register(cls.name, cls) def __setattr__(cls, name: str, value: any) -> None: @@ -92,8 +94,8 @@ def __eq__(cls, other: any) -> bool: cls_name = getattr(cls, "name", None) other_name = getattr(other, "name", None) - cls_namespace = getattr(cls, "_namespace", "std") - other_namespace = getattr(other, "_namespace", "std") + cls_namespace = getattr(cls, "namespace", None) + other_namespace = getattr(other, "namespace", None) # They are equal if they share the same name and namespace return cls_name == other_name and cls_namespace == other_namespace @@ -161,7 +163,7 @@ class attribute for subclasses. # 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 + namespace: NameSpace name: str num_qubits: int @@ -184,6 +186,11 @@ def __init_subclass__(cls, **kwargs): if inspect.isabstract(cls): return + if getattr(cls, "namespace", None) is None: + raise AttributeError( + f"Missing attribute namespace from {cls.__name__}" + ) + # If name attribute in subclass is not defined, set it to the name of the subclass # e.g. class H(Gate): # pass @@ -276,7 +283,7 @@ def is_parametric() -> bool: def get_unitary_gate( - gate_name: str, U: Qobj, namespace: NameSpace = NS_USER + gate_name: str, U: Qobj, gate_namespace: NameSpace = NS_USER ) -> Type[Gate]: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. @@ -295,7 +302,7 @@ def get_unitary_gate( class _CustomGate(Gate): __slots__ = () - _namespace = namespace + namespace = gate_namespace name = gate_name num_qubits = int(n) self_inverse = U == U.dag() diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index b13405799..f816c84cb 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -20,7 +20,7 @@ class GLOBALPHASE(AngleParametricGate): """ __slots__ = "phase" - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 0 num_params: Final[int] = 1 @@ -69,7 +69,7 @@ class TOFFOLI(ControlledGate): """ __slots__ = () - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 3 num_ctrl_qubits: Final[int] = 2 @@ -118,7 +118,7 @@ class FREDKIN(ControlledGate): """ __slots__ = () - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 3 num_ctrl_qubits: Final[int] = 1 diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 16432ed54..d27a1e630 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -5,14 +5,14 @@ from qutip import Qobj, sigmax, sigmay, sigmaz, qeye from qutip_qip.operations import Gate, AngleParametricGate -from qutip_qip.operations.namespace import NS_GATE +from qutip_qip.operations.namespace import NS_GATE, NameSpace class _SingleQubitGate(Gate): """Abstract one-qubit gate.""" __slots__ = () - _namespace = NS_GATE + namespace: NameSpace = NS_GATE num_qubits: Final[int] = 1 @@ -20,6 +20,7 @@ class _SingleQubitParametricGate(AngleParametricGate): """Abstract one-qubit parametric gate.""" __slots__ = () + namespace: NameSpace = NS_GATE num_qubits: Final[int] = 1 diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 3185b3b75..72fb1bee5 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -26,7 +26,7 @@ class _TwoQubitGate(Gate): """Abstract two-qubit gate.""" __slots__ = () - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 2 @@ -34,7 +34,7 @@ class _TwoQubitParametricGate(AngleParametricGate): """Abstract two-qubit Parametric Gate (non-controlled).""" __slots__ = () - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 2 @@ -42,7 +42,7 @@ class _ControlledTwoQubitGate(ControlledGate): """Abstract two-qubit Controlled Gate (both parametric and non-parametric).""" __slots__ = () - _namespace = NS_GATE + namespace = NS_GATE num_qubits: Final[int] = 2 num_ctrl_qubits: Final[int] = 1 diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index 7863267e4..a103e98b8 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -56,6 +56,19 @@ def register(self, name: str, item: any) -> None: """Safely adds an item to the specific namespace.""" name_upper = name.upper() if name_upper in self._registry: + existing_item = self._registry[name_upper] + + # Tolerate harmless reloads from pytest double-imports or Jupyter cell re-runs + if getattr(existing_item, "__module__", None) == getattr( + item, "__module__", None + ) and getattr(existing_item, "__name__", None) == getattr( + item, "__name__", None + ): + + # Silently overwrite with the fresh reloaded class and continue + self._registry[name_upper] = item + return + raise ValueError( f"'{name_upper}' already exists in namespace '{self.name}'" ) diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 4f03e3318..895c5d174 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -146,7 +146,7 @@ def construct_circuit(self, angles): current_params = angles[i : i + n] if n > 0 else [] gate_instance = get_unitary_gate( gate_name=block.name, - namespace="vqa", + # namespace="vqa", U=block.get_unitary(current_params), ) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 97228a27b..00fc6dbf1 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -167,7 +167,7 @@ def test_add_gate(self): assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): - _namespace = NS_USER + namespace = NS_USER num_qubits = 1 self_inverse = False @@ -421,7 +421,7 @@ def customer_gate1(arg_values): return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): - _namespace = NS_USER + namespace = NS_USER num_qubits = 1 self_inverse = True @@ -449,7 +449,7 @@ def test_N_level_system(self): mat3 = qp.rand_unitary(3) class CTRLMAT3(Gate): - _namespace = NS_USER + namespace = NS_USER num_qubits = 2 self_inverse = False diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 653f1f366..24e415940 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,7 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import AngleParametricGate +from qutip_qip.operations import AngleParametricGate, NS_USER from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity @@ -81,6 +81,7 @@ def two_qubit_gate_compiler(self, circuit_instruction, args): ] class U1(AngleParametricGate): + namespace = NS_USER num_qubits = 1 num_params = 1 self_inverse = False @@ -89,6 +90,7 @@ def _compute_qobj(self): pass class U2(AngleParametricGate): + namespace = NS_USER num_qubits = 2 num_params = 1 self_inverse = False From b74497b253e3c55732bc3713a686fe9a73ea3efa Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Thu, 5 Mar 2026 04:46:18 +0530 Subject: [PATCH 084/117] Move pytest.ini to root dir, make namespace a set pytest.ini was causing several issues the way it handles import paths as strings using sys.module. Moving it to the root and adding pythonpath=src, testpaths=tests resolves this issue. --- tests/pytest.ini => pytest.ini | 5 ++--- src/qutip_qip/operations/gateclass.py | 2 +- src/qutip_qip/operations/namespace.py | 29 +++------------------------ 3 files changed, 6 insertions(+), 30 deletions(-) rename tests/pytest.ini => pytest.ini (95%) diff --git a/tests/pytest.ini b/pytest.ini similarity index 95% rename from tests/pytest.ini rename to pytest.ini index b2071fe08..077fbf649 100644 --- a/tests/pytest.ini +++ b/pytest.ini @@ -1,7 +1,6 @@ [pytest] markers = slow: Mark a test as taking a long time to run, and so can be skipped with `pytest -m "not slow"`. - repeat(n): Repeat the given test 'n' times. requires_cython: Mark that the given test requires Cython to be installed. Such tests will be skipped if Cython is not available. filterwarnings = @@ -18,5 +17,5 @@ filterwarnings = # Deprecation warning for scipy disp interface, will be removed in scipy 1.18 ignore:.*`disp` and `iprint` options.*L-BFGS-B.*deprecated.*:DeprecationWarning - - +pythonpath = src +testpaths = tests diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 874c5d68a..452e475b8 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -50,7 +50,7 @@ def __init__(cls, name, bases, attrs): # We obviously don't register abstract classes to the namespace. namespace = cls.namespace - namespace.register(cls.name, cls) + namespace.register(cls.name) def __setattr__(cls, name: str, value: any) -> None: """ diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index a103e98b8..a9b352d90 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -36,7 +36,7 @@ def register_namespace(self, namespace: NameSpace) -> None: class NameSpace: local_name: str parent: NameSpace | None = None - _registry: dict[str, any] = field(default_factory=dict) + _registry: set[str] = field(default_factory=set) def __post_init__(self): if "." in self.local_name: @@ -52,37 +52,14 @@ def name(self) -> str: return f"{self.parent.name}.{self.local_name}" return self.local_name - def register(self, name: str, item: any) -> None: + def register(self, name: str) -> None: """Safely adds an item to the specific namespace.""" name_upper = name.upper() if name_upper in self._registry: - existing_item = self._registry[name_upper] - - # Tolerate harmless reloads from pytest double-imports or Jupyter cell re-runs - if getattr(existing_item, "__module__", None) == getattr( - item, "__module__", None - ) and getattr(existing_item, "__name__", None) == getattr( - item, "__name__", None - ): - - # Silently overwrite with the fresh reloaded class and continue - self._registry[name_upper] = item - return - raise ValueError( f"'{name_upper}' already exists in namespace '{self.name}'" ) - - self._registry[name_upper] = item - - def get(self, name: str) -> any: - """Retrieves an item from a specific namespace.""" - try: - return self._registry[name.upper()] - except KeyError: - raise KeyError( - f"'{name.upper()}' not found in namespace '{self.name}'." - ) + self._registry.add(name_upper) def __hash__(self) -> int: return hash(self.name) From b1cbdaa3cf773c1800a7fc2ff0b3c7a673773106 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Thu, 5 Mar 2026 05:17:34 +0530 Subject: [PATCH 085/117] Minor fixes to tests, ControlledGate.inverse method --- src/qutip_qip/algorithms/qpe.py | 2 +- src/qutip_qip/operations/__init__.py | 4 ++-- src/qutip_qip/operations/controlled.py | 7 +++++-- src/qutip_qip/operations/gateclass.py | 25 ++++--------------------- src/qutip_qip/operations/namespace.py | 10 ++++++---- src/qutip_qip/vqa.py | 2 +- tests/test_circuit.py | 8 ++++---- tests/test_compiler.py | 6 +++--- tests/test_qasm.py | 7 +++---- tests/test_vqa.py | 6 ++++++ 10 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 3de07620b..01960de0d 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -55,7 +55,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): for i in range(num_counting_qubits): qc.add_gate(H, targets=[i]) - Gate.clear_cache("qpe") + # Gate.clear_cache("qpe") # Apply controlled-U gates with increasing powers for i in range(num_counting_qubits): power = 2 ** (num_counting_qubits - i - 1) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index c2c4ca959..35cbd0b1b 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -3,9 +3,9 @@ """ from .namespace import ( - GlobalNameSpaceRegistry, NameSpace, NS_USER, + NS_USER_GATES, ) from .utils import ( hadamard_transform, @@ -54,8 +54,8 @@ __all__ = [ "NameSpace", - "GlobalNameSpaceRegistry", "NS_USER", + "NS_USER_GATES", "Gate", "ParametricGate", "ControlledGate", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index c6446daeb..d4da196f0 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -7,7 +7,7 @@ from qutip_qip.operations import ( Gate, NameSpace, - NS_USER, + NS_USER_GATES, controlled_gate_unitary, ) @@ -190,6 +190,9 @@ def get_qobj(cls_or_self) -> Qobj: @class_or_instance_method def inverse(cls_or_self) -> Gate | Type[Gate]: + if cls_or_self.self_inverse: + return cls_or_self + if isinstance(cls_or_self, type): return controlled( cls_or_self.target_gate.inverse(), @@ -243,7 +246,7 @@ def controlled( n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, - gate_namespace: NameSpace = NS_USER, + gate_namespace: NameSpace = NS_USER_GATES, ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 452e475b8..4ebed48d0 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -6,7 +6,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations.namespace import NameSpace, NS_USER +from qutip_qip.operations.namespace import NameSpace, NS_USER_GATES _read_only_set: set[str] = set( ( @@ -24,8 +24,6 @@ class _GateMetaClass(ABCMeta): - _registry: dict[str, set] = {} - def __init__(cls, name, bases, attrs): """ This method is automatically invoked during class creation. It validates that @@ -40,7 +38,7 @@ def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) # Don't register the Abstract Gate Classes or private helpers - if inspect.isabstract(cls) or name.startswith("_"): + if inspect.isabstract(cls): cls._is_frozen = True return @@ -71,7 +69,7 @@ class X(Gate): # accidentally inherit the True flag from a parent class for _is_frozen. if ( cls.__dict__.get("_is_frozen", False) - and name in cls._read_only_set + and name in _read_only_set ): raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) @@ -112,21 +110,6 @@ def __hash__(cls) -> int: (getattr(cls, "namespace", "std"), getattr(cls, "name", None)) ) - def clear_cache(cls, namespace: str): - """ - Clears the gate class registry based on the namespace. - - Parameters - ---------- - namespace : str, optional - If provided, only clears gates belonging to this namespace - (e.g., 'custom'). If None, clears ALL gates (useful for hard resets). - """ - if namespace == "std": - raise ValueError("Can't clear std Gates") - else: - cls._registry[namespace] = set() - class Gate(ABC, metaclass=_GateMetaClass): r""" @@ -283,7 +266,7 @@ def is_parametric() -> bool: def get_unitary_gate( - gate_name: str, U: Qobj, gate_namespace: NameSpace = NS_USER + gate_name: str, U: Qobj, gate_namespace: NameSpace = NS_USER_GATES ) -> Type[Gate]: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index a9b352d90..b43f21c6b 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -18,7 +18,7 @@ def __call__(cls, *args, **kwargs): return cls._instances[cls] -class _GlobalRegistry(metaclass=_SingletonMeta): +class _GlobalNameSpaceRegistry(metaclass=_SingletonMeta): def __init__(self): self._registry: set[NameSpace] = set() @@ -29,7 +29,7 @@ def register_namespace(self, namespace: NameSpace) -> None: self._registry.add(namespace) -GlobalNameSpaceRegistry = _GlobalRegistry() +_GlobalRegistry = _GlobalNameSpaceRegistry() @dataclass @@ -44,7 +44,7 @@ def __post_init__(self): f"Namespace local_name '{self.local_name}' cannot contain dots. " f"Dots are reserved for hierarchical resolution." ) - GlobalNameSpaceRegistry.register_namespace(self) + _GlobalRegistry.register_namespace(self) @cached_property def name(self) -> str: @@ -56,7 +56,7 @@ def register(self, name: str) -> None: """Safely adds an item to the specific namespace.""" name_upper = name.upper() if name_upper in self._registry: - raise ValueError( + raise NameError( f"'{name_upper}' already exists in namespace '{self.name}'" ) self._registry.add(name_upper) @@ -73,4 +73,6 @@ def __repr__(self): NS_STD = NameSpace("std") # DEFAULT NAMESPACE NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace + NS_USER = NameSpace("user") # Default for anything defined by the user +NS_USER_GATES = NameSpace("gates", parent=NS_USER) # Any user gates defined diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 895c5d174..362467260 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -134,7 +134,7 @@ def construct_circuit(self, angles): circ = QubitCircuit(self.num_qubits) i = 0 for layer_num in range(self.num_layers): - Gate.clear_cache("vqa") + # Gate.clear_cache("vqa") for block in self.blocks: if block.initial and layer_num > 0: continue diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 00fc6dbf1..01a1a958f 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -25,7 +25,7 @@ Gate, Measurement, gate_sequence_product, - NS_USER, + NS_USER_GATES, ) import qutip_qip.operations.gates as gates from qutip_qip.transpiler import to_chain_structure @@ -167,7 +167,7 @@ def test_add_gate(self): assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): - namespace = NS_USER + namespace = NS_USER_GATES num_qubits = 1 self_inverse = False @@ -421,7 +421,7 @@ def customer_gate1(arg_values): return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): - namespace = NS_USER + namespace = NS_USER_GATES num_qubits = 1 self_inverse = True @@ -449,7 +449,7 @@ def test_N_level_system(self): mat3 = qp.rand_unitary(3) class CTRLMAT3(Gate): - namespace = NS_USER + namespace = NS_USER_GATES num_qubits = 2 self_inverse = False diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 24e415940..f791abf4b 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,7 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import AngleParametricGate, NS_USER +from qutip_qip.operations import AngleParametricGate, NS_USER_GATES from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity @@ -81,7 +81,7 @@ def two_qubit_gate_compiler(self, circuit_instruction, args): ] class U1(AngleParametricGate): - namespace = NS_USER + namespace = NS_USER_GATES num_qubits = 1 num_params = 1 self_inverse = False @@ -90,7 +90,7 @@ def _compute_qobj(self): pass class U2(AngleParametricGate): - namespace = NS_USER + namespace = NS_USER_GATES num_qubits = 2 num_params = 1 self_inverse = False diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 135fd0ac9..a2d2cee85 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -7,7 +7,7 @@ from qutip_qip.qasm import read_qasm, circuit_to_qasm_str from qutip_qip.circuit import QubitCircuit from qutip import tensor, rand_ket, basis, identity -from qutip_qip.operations import Measurement, Gate +from qutip_qip.operations import Measurement import qutip_qip.operations.gates as gates @@ -172,17 +172,16 @@ def test_export_import(): def test_read_qasm_1(): - Gate.clear_cache(namespace="custom") filename = "w-state.qasm" filepath = Path(__file__).parent / "qasm_files" / filename read_qasm(filepath) def test_read_qasm_2(): - Gate.clear_cache(namespace="custom") filename2 = "w-state_with_comments.qasm" filepath2 = Path(__file__).parent / "qasm_files" / filename2 - read_qasm(filepath2) + with pytest.raises(NameError, match="'CH' already exists in namespace 'user.gates'"): + read_qasm(filepath2) def test_parsing_mode(tmp_path): diff --git a/tests/test_vqa.py b/tests/test_vqa.py index 822b71956..8a862dbc5 100644 --- a/tests/test_vqa.py +++ b/tests/test_vqa.py @@ -3,6 +3,7 @@ import qutip from qutip_qip.operations import expand_operator from qutip_qip.operations.gates import H +from qutip_qip.operations.namespace import NS_GATE, NS_USER_GATES from qutip_qip.vqa import ( VQA, VQABlock, @@ -241,3 +242,8 @@ class fakeRes: S = [1, 2, 3] result = OptimizationResult(fakeScipyRes, qutip.basis(8, 1)) assert result._label_to_sets(S, "|010>") == "{1, 3} {2}" + + print(NS_USER_GATES._registry) + print() + print(NS_GATE._registry) + assert False From a3a1674f4c2aa8dbb8714ab9548713affb32b88f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 7 Mar 2026 05:41:51 +0530 Subject: [PATCH 086/117] Fix the namespace mechanism --- src/qutip_qip/algorithms/qpe.py | 3 +-- src/qutip_qip/operations/__init__.py | 8 +------- src/qutip_qip/operations/controlled.py | 6 ++++-- src/qutip_qip/operations/gateclass.py | 23 ++++++++--------------- src/qutip_qip/operations/namespace.py | 19 +++++++++++-------- src/qutip_qip/vqa.py | 5 ++--- tests/test_circuit.py | 10 +--------- tests/test_compiler.py | 4 +--- tests/test_qasm.py | 3 +-- tests/test_vqa.py | 6 ------ 10 files changed, 30 insertions(+), 57 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 01960de0d..1a702229e 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,7 @@ import numpy as np from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import get_unitary_gate, controlled, Gate +from qutip_qip.operations import get_unitary_gate, controlled from qutip_qip.operations.gates import H @@ -66,7 +66,6 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): controlled_u = controlled( gate=get_unitary_gate( gate_name=f"U^{power}", - # namespace="qpe", U=U_power, ), ) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 35cbd0b1b..5b46c7bb2 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -2,11 +2,7 @@ Operations on quantum circuits. """ -from .namespace import ( - NameSpace, - NS_USER, - NS_USER_GATES, -) +from .namespace import NameSpace from .utils import ( hadamard_transform, expand_operator, @@ -54,8 +50,6 @@ __all__ = [ "NameSpace", - "NS_USER", - "NS_USER_GATES", "Gate", "ParametricGate", "ControlledGate", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index d4da196f0..6f008a4d1 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -7,7 +7,6 @@ from qutip_qip.operations import ( Gate, NameSpace, - NS_USER_GATES, controlled_gate_unitary, ) @@ -246,12 +245,15 @@ def controlled( n_ctrl_qubits: int = 1, control_value: int | None = None, gate_name: str | None = None, - gate_namespace: NameSpace = NS_USER_GATES, + gate_namespace: NameSpace | None = None, ) -> ControlledGate: """ Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ + # if gate_namespace is None: + # gate_namespace = gate.namespace + if gate_name is None: # print(n_ctrl_qubits) # gate_name = f"{'C'*{n_ctrl_qubits}}{gate.name}" diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 4ebed48d0..063624f0c 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -6,7 +6,7 @@ import numpy as np from qutip import Qobj -from qutip_qip.operations.namespace import NameSpace, NS_USER_GATES +from qutip_qip.operations.namespace import NameSpace _read_only_set: set[str] = set( ( @@ -46,9 +46,10 @@ def __init__(cls, name, bases, attrs): # don't overwrite any defaults like num_qubits etc in __setattr__. cls._is_frozen = True - # We obviously don't register abstract classes to the namespace. - namespace = cls.namespace - namespace.register(cls.name) + # Namespace being None corresponds to Temporary Gates + # Only if it is provided register it + if getattr(cls, "namespace", None) is not None: + cls.namespace.register(cls) def __setattr__(cls, name: str, value: any) -> None: """ @@ -67,10 +68,7 @@ class X(Gate): """ # 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 - ): + if cls.__dict__.get("_is_frozen", False) and name in _read_only_set: raise AttributeError(f"{name} is read-only!") super().__setattr__(name, value) @@ -146,7 +144,7 @@ class attribute for subclasses. # 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 + namespace: NameSpace | None = None name: str num_qubits: int @@ -169,11 +167,6 @@ def __init_subclass__(cls, **kwargs): if inspect.isabstract(cls): return - if getattr(cls, "namespace", None) is None: - raise AttributeError( - f"Missing attribute namespace from {cls.__name__}" - ) - # If name attribute in subclass is not defined, set it to the name of the subclass # e.g. class H(Gate): # pass @@ -266,7 +259,7 @@ def is_parametric() -> bool: def get_unitary_gate( - gate_name: str, U: Qobj, gate_namespace: NameSpace = NS_USER_GATES + gate_name: str, U: Qobj, gate_namespace: NameSpace | None = None ) -> Type[Gate]: """ Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index b43f21c6b..e700c0dca 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -26,8 +26,15 @@ def register_namespace(self, namespace: NameSpace) -> None: """Safely adds an item to the specific namespace.""" if namespace in self._registry: raise ValueError(f"Existing namespace {namespace}") + + # Note: This does mean that gate (or operation) is never garbage + # collected until the Namespace exists. This is fine for standard gates. self._registry.add(namespace) + # Default behaviour for user defining his own gates is that namespace is None, + # Thus those gates are considered temporary by default, we use the same logic in + # QPE for Controlled Unitary gates, VQA (until Ops i.e. composite gates are introduced). + _GlobalRegistry = _GlobalNameSpaceRegistry() @@ -52,14 +59,13 @@ def name(self) -> str: return f"{self.parent.name}.{self.local_name}" return self.local_name - def register(self, name: str) -> None: + def register(self, operation_cls: any) -> None: """Safely adds an item to the specific namespace.""" - name_upper = name.upper() - if name_upper in self._registry: + if operation_cls in self._registry: raise NameError( - f"'{name_upper}' already exists in namespace '{self.name}'" + f"'{operation_cls.name}' already exists in namespace '{self.name}'" ) - self._registry.add(name_upper) + self._registry.add(operation_cls) def __hash__(self) -> int: return hash(self.name) @@ -73,6 +79,3 @@ def __repr__(self): NS_STD = NameSpace("std") # DEFAULT NAMESPACE NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace - -NS_USER = NameSpace("user") # Default for anything defined by the user -NS_USER_GATES = NameSpace("gates", parent=NS_USER) # Any user gates defined diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 362467260..4314db6f1 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -10,7 +10,7 @@ from scipy.linalg import expm_frechet from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import Gate, gate_sequence_product, get_unitary_gate +from qutip_qip.operations import gate_sequence_product, get_unitary_gate class VQA: @@ -145,8 +145,7 @@ def construct_circuit(self, angles): current_params = angles[i : i + n] if n > 0 else [] gate_instance = get_unitary_gate( - gate_name=block.name, - # namespace="vqa", + gate_name=f"{block.name}{layer_num}", U=block.get_unitary(current_params), ) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 01a1a958f..77221195f 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -21,12 +21,7 @@ from qutip_qip.circuit import QubitCircuit, CircuitSimulator from qutip_qip.circuit.draw import TeXRenderer from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation -from qutip_qip.operations import ( - Gate, - Measurement, - gate_sequence_product, - NS_USER_GATES, -) +from qutip_qip.operations import Gate, Measurement, gate_sequence_product import qutip_qip.operations.gates as gates from qutip_qip.transpiler import to_chain_structure from qutip_qip.qasm import read_qasm @@ -167,7 +162,6 @@ def test_add_gate(self): assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): - namespace = NS_USER_GATES num_qubits = 1 self_inverse = False @@ -421,7 +415,6 @@ def customer_gate1(arg_values): return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): - namespace = NS_USER_GATES num_qubits = 1 self_inverse = True @@ -449,7 +442,6 @@ def test_N_level_system(self): mat3 = qp.rand_unitary(3) class CTRLMAT3(Gate): - namespace = NS_USER_GATES num_qubits = 2 self_inverse = False diff --git a/tests/test_compiler.py b/tests/test_compiler.py index f791abf4b..653f1f366 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,7 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import AngleParametricGate, NS_USER_GATES +from qutip_qip.operations import AngleParametricGate from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity @@ -81,7 +81,6 @@ def two_qubit_gate_compiler(self, circuit_instruction, args): ] class U1(AngleParametricGate): - namespace = NS_USER_GATES num_qubits = 1 num_params = 1 self_inverse = False @@ -90,7 +89,6 @@ def _compute_qobj(self): pass class U2(AngleParametricGate): - namespace = NS_USER_GATES num_qubits = 2 num_params = 1 self_inverse = False diff --git a/tests/test_qasm.py b/tests/test_qasm.py index a2d2cee85..603235850 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -180,8 +180,7 @@ def test_read_qasm_1(): def test_read_qasm_2(): filename2 = "w-state_with_comments.qasm" filepath2 = Path(__file__).parent / "qasm_files" / filename2 - with pytest.raises(NameError, match="'CH' already exists in namespace 'user.gates'"): - read_qasm(filepath2) + read_qasm(filepath2) def test_parsing_mode(tmp_path): diff --git a/tests/test_vqa.py b/tests/test_vqa.py index 8a862dbc5..822b71956 100644 --- a/tests/test_vqa.py +++ b/tests/test_vqa.py @@ -3,7 +3,6 @@ import qutip from qutip_qip.operations import expand_operator from qutip_qip.operations.gates import H -from qutip_qip.operations.namespace import NS_GATE, NS_USER_GATES from qutip_qip.vqa import ( VQA, VQABlock, @@ -242,8 +241,3 @@ class fakeRes: S = [1, 2, 3] result = OptimizationResult(fakeScipyRes, qutip.basis(8, 1)) assert result._label_to_sets(S, "|010>") == "{1, 3} {2}" - - print(NS_USER_GATES._registry) - print() - print(NS_GATE._registry) - assert False From b90f53b7c34625b0252d0183016f4681357a6d57 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 7 Mar 2026 06:24:00 +0530 Subject: [PATCH 087/117] Corrected inverse method for controlled, parametrized gates --- src/qutip_qip/operations/controlled.py | 50 ++++++++++--------- .../operations/gates/single_qubit_gate.py | 6 +++ .../operations/gates/two_qubit_gate.py | 3 ++ src/qutip_qip/operations/parametric.py | 2 - tests/test_gates.py | 3 ++ 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 6f008a4d1..8ea43b224 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -189,32 +189,36 @@ def get_qobj(cls_or_self) -> Qobj: @class_or_instance_method def inverse(cls_or_self) -> Gate | Type[Gate]: - if cls_or_self.self_inverse: - return cls_or_self + # Non-parametrized Gates e.g. S + if not cls_or_self.is_parametric(): + if cls_or_self.self_inverse: + inverse_gate = cls_or_self + + else: + inverse_gate = controlled( + cls_or_self.target_gate.inverse(), + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + ) - if isinstance(cls_or_self, type): - return controlled( - cls_or_self.target_gate.inverse(), - cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value, - ) - - elif ( - isinstance(cls_or_self, object) - and cls_or_self._target_inst.is_parametric() - ): + else: inverse_gate_class, param = cls_or_self._target_inst.inverse( expanded=True ) - inverse = controlled( - inverse_gate_class, - cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value, - ) - return inverse(*param) - else: - raise NotImplementedError + if cls_or_self.self_inverse: + # RX has the same class inverse RX but with different parameter + # So we shouldn't ideally redefine the class RX (redundant). + inverse_gate = type(cls_or_self)(*param) + + else: + inverse_gate = controlled( + inverse_gate_class, + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + )(*param) + + return inverse_gate @staticmethod def is_controlled() -> bool: @@ -251,8 +255,8 @@ def controlled( Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ - # if gate_namespace is None: - # gate_namespace = gate.namespace + if gate_namespace is None: + gate_namespace = gate.namespace if gate_name is None: # print(n_ctrl_qubits) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index d27a1e630..f444025e2 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -374,6 +374,7 @@ class RX(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"R_x" def __init__(self, theta: float, arg_label=None): @@ -418,6 +419,7 @@ class RY(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"R_y" def __init__(self, theta: float, arg_label: str | None = None): @@ -461,6 +463,7 @@ class RZ(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"R_z" def __init__(self, theta: float, arg_label: str | None = None): @@ -494,6 +497,7 @@ class PHASE(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"PHASE" def __init__(self, theta: float, arg_label: str | None = None): @@ -544,6 +548,7 @@ class R(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 2 + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm R}" def __init__(self, phi: float, theta: float, arg_label: str | None = None): @@ -598,6 +603,7 @@ class QASMU(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 3 + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm QASMU}" def __init__( diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 72fb1bee5..9e1b10b25 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -440,6 +440,7 @@ class SWAPALPHA(_TwoQubitParametricGate): __slots__ = "alpha" num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm SWAPALPHA}" def __init__(self, alpha: float, arg_label: str | None = None): @@ -507,6 +508,7 @@ class MS(_TwoQubitParametricGate): __slots__ = ("theta", "phi") num_params: Final[int] = 2 + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm MS}" def __init__(self, theta: float, phi: float, arg_label: str | None = None): @@ -576,6 +578,7 @@ class RZX(_TwoQubitParametricGate): __slots__ = "theta" num_params: Final[int] = 1 + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm RZX}" def __init__(self, theta: float, arg_label: str | None = None): diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index cbddd4c12..1d93bf072 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -119,8 +119,6 @@ def _compute_qobj(args: tuple) -> "Qobj": pass def inverse(self, expanded: bool = False) -> Gate: - if self.self_inverse: - return self raise NotImplementedError @staticmethod diff --git a/tests/test_gates.py b/tests/test_gates.py index f8a168727..1595a93ec 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -511,6 +511,9 @@ def test_gates_class(): def test_gate_inverse(gate: Gate | Type[Gate]): n = 2**gate.num_qubits inverse = gate.inverse() + print(gate.name) + print(gate.get_qobj()) + print(inverse.get_qobj()) np.testing.assert_allclose( (gate.get_qobj() * inverse.get_qobj()).full(), np.eye(n), From d7ab53a3c214b5df48c6b67dcc0118f52dd8ba09 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 7 Mar 2026 07:10:10 +0530 Subject: [PATCH 088/117] Added a controlled gate registry mechanism --- src/qutip_qip/operations/controlled.py | 52 +++++++++---------- src/qutip_qip/operations/gateclass.py | 11 +++- .../operations/gates/single_qubit_gate.py | 6 --- .../operations/gates/two_qubit_gate.py | 27 +--------- src/qutip_qip/operations/namespace.py | 20 +++++-- src/qutip_qip/operations/parametric.py | 2 + tests/test_renderer.py | 2 +- tests/test_scheduler.py | 3 +- 8 files changed, 56 insertions(+), 67 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 8ea43b224..6bb509292 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -55,7 +55,7 @@ class ControlledGate(Gate): (binary 10), set ``ctrl_value=0b10``. """ - __slots__ = "_target_inst" + __slots__ = ("_target_inst",) num_ctrl_qubits: int ctrl_value: int @@ -189,34 +189,27 @@ def get_qobj(cls_or_self) -> Qobj: @class_or_instance_method def inverse(cls_or_self) -> Gate | Type[Gate]: - # Non-parametrized Gates e.g. S - if not cls_or_self.is_parametric(): - if cls_or_self.self_inverse: - inverse_gate = cls_or_self + if cls_or_self.self_inverse: + inverse_gate = cls_or_self - else: - inverse_gate = controlled( - cls_or_self.target_gate.inverse(), - cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value, - ) + # Non-parametrized Gates e.g. S + if isinstance(cls_or_self, type): + inverse_gate = controlled( + cls_or_self.target_gate.inverse(), + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + ) else: inverse_gate_class, param = cls_or_self._target_inst.inverse( expanded=True ) - if cls_or_self.self_inverse: - # RX has the same class inverse RX but with different parameter - # So we shouldn't ideally redefine the class RX (redundant). - inverse_gate = type(cls_or_self)(*param) - - else: - inverse_gate = controlled( - inverse_gate_class, - cls_or_self.num_ctrl_qubits, - cls_or_self.ctrl_value, - )(*param) + inverse_gate = controlled( + inverse_gate_class, + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + )(*param) return inverse_gate @@ -258,14 +251,19 @@ def controlled( if gate_namespace is None: gate_namespace = gate.namespace - if gate_name is None: - # print(n_ctrl_qubits) - # gate_name = f"{'C'*{n_ctrl_qubits}}{gate.name}" - gate_name = f"{'C'}{gate.name}" - if control_value is None: control_value = 2**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: + return found_gate + class _CustomControlledGate(ControlledGate): __slots__ = () namespace = gate_namespace diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 063624f0c..55a480f83 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -49,7 +49,16 @@ def __init__(cls, name, bases, attrs): # Namespace being None corresponds to Temporary Gates # Only if it is provided register it if getattr(cls, "namespace", None) is not None: - cls.namespace.register(cls) + cls.namespace.register(cls.name, cls) + + # For lookup dictionary for Controlled Gates + # e.g. controlled(X, num_ctrl_qubits=1, ctrl_value=1) this is CX, + # why do we have to define it again if it already exists. + if getattr(cls, "namespace", None) is not None and cls.is_controlled(): + cls.namespace.register( + (cls.target_gate.name, cls.num_ctrl_qubits, cls.ctrl_value), + cls, + ) def __setattr__(cls, name: str, value: any) -> None: """ diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index f444025e2..d27a1e630 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -374,7 +374,6 @@ class RX(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"R_x" def __init__(self, theta: float, arg_label=None): @@ -419,7 +418,6 @@ class RY(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"R_y" def __init__(self, theta: float, arg_label: str | None = None): @@ -463,7 +461,6 @@ class RZ(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"R_z" def __init__(self, theta: float, arg_label: str | None = None): @@ -497,7 +494,6 @@ class PHASE(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"PHASE" def __init__(self, theta: float, arg_label: str | None = None): @@ -548,7 +544,6 @@ class R(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 2 - self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm R}" def __init__(self, phi: float, theta: float, arg_label: str | None = None): @@ -603,7 +598,6 @@ class QASMU(_SingleQubitParametricGate): __slots__ = () num_params: Final[int] = 3 - self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm QASMU}" def __init__( diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 9e1b10b25..df2743ac7 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -440,7 +440,6 @@ class SWAPALPHA(_TwoQubitParametricGate): __slots__ = "alpha" num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm SWAPALPHA}" def __init__(self, alpha: float, arg_label: str | None = None): @@ -508,7 +507,6 @@ class MS(_TwoQubitParametricGate): __slots__ = ("theta", "phi") num_params: Final[int] = 2 - self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm MS}" def __init__(self, theta: float, phi: float, arg_label: str | None = None): @@ -578,7 +576,6 @@ class RZX(_TwoQubitParametricGate): __slots__ = "theta" num_params: Final[int] = 1 - self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm RZX}" def __init__(self, theta: float, arg_label: str | None = None): @@ -641,17 +638,7 @@ def get_qobj() -> Qobj: ) -class CNOT(CX): - __slots__ = () - - def __init__(self): - warnings.warn( - "CNOT is deprecated and will be removed in future versions. " - "Use CX instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() +CNOT = CX class CY(_ControlledTwoQubitGate): @@ -716,17 +703,7 @@ def get_qobj() -> Qobj: ) -class CSIGN(CZ): - __slots__ = () - - def __init__(self): - warnings.warn( - "CSIGN is deprecated and will be removed in future versions. " - "Use CZ instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() +CSIGN = CZ class CH(_ControlledTwoQubitGate): diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index e700c0dca..e07623126 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -43,7 +43,7 @@ def register_namespace(self, namespace: NameSpace) -> None: class NameSpace: local_name: str parent: NameSpace | None = None - _registry: set[str] = field(default_factory=set) + _registry: dict[str, any] = field(default_factory=dict) def __post_init__(self): if "." in self.local_name: @@ -59,13 +59,23 @@ def name(self) -> str: return f"{self.parent.name}.{self.local_name}" return self.local_name - def register(self, operation_cls: any) -> None: - """Safely adds an item to the specific namespace.""" - if operation_cls in self._registry: + def register( + self, name: str | tuple[str, int, int], operation_cls: any + ) -> None: + """Safely adds an item to the specific namespace. + name is str for a non-controlled Gate + name is a tuple (target_gate.name, num_ctrl_qubits, ctrl_values) for a Controlled gate. + """ + if name in self._registry: raise NameError( f"'{operation_cls.name}' already exists in namespace '{self.name}'" ) - self._registry.add(operation_cls) + self._registry[name] = operation_cls + + def get(self, name: str | tuple[str, int, int]) -> any: + if name not in self._registry: + return None + return self._registry[name] def __hash__(self) -> int: return hash(self.name) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 1d93bf072..cbddd4c12 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -119,6 +119,8 @@ def _compute_qobj(args: tuple) -> "Qobj": pass def inverse(self, expanded: bool = False) -> Gate: + if self.self_inverse: + return self raise NotImplementedError @staticmethod diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 737643582..9639c0190 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -166,7 +166,7 @@ def qc3(): def qc4(): i = controlled(IDLE, n_ctrl_qubits=1, gate_name="i") ii = controlled(IDLE, n_ctrl_qubits=2, gate_name="ii") - iii = controlled(IDLE, n_ctrl_qubits=1, gate_name="iii") + iii = controlled(IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii") qc = QubitCircuit(5, num_cbits=2) qc.add_gate( diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 8791a355e..483790e3c 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -3,7 +3,6 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.compiler import PulseInstruction, Scheduler -from qutip_qip.operations import ControlledGate from qutip_qip.operations.gates import ( GATE_CLASS_MAP, CX, @@ -277,7 +276,7 @@ def test_scheduling_pulse( for instruction in instructions: gate_cls = GATE_CLASS_MAP[instruction.name] - if issubclass(gate_cls, ControlledGate): + if gate_cls.is_controlled(): circuit.add_gate( gate_cls, targets=instruction.targets, From ad7782dfe7e977e84c50e7fd44c13bf948678018 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sat, 7 Mar 2026 07:52:14 +0530 Subject: [PATCH 089/117] Add additional checks on Gate subclasses inverse method can't be defined if self_inverse is set True check is_clifford, self_inverse is of boolean type --- src/qutip_qip/operations/controlled.py | 3 --- src/qutip_qip/operations/gateclass.py | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 6bb509292..4210b8109 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -189,9 +189,6 @@ def get_qobj(cls_or_self) -> Qobj: @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 if isinstance(cls_or_self, type): inverse_gate = controlled( diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 55a480f83..7ebd694ad 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -199,10 +199,32 @@ def __init_subclass__(cls, **kwargs): 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"Class '{cls.name}' attribute 'num_qubits' must be a non-negative integer, " f"got {type(num_qubits)} with value {num_qubits}." ) + # 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}." + ) + + # 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." + ) + def __init__(self) -> None: """ This method is overwritten by Parametrized and Controlled Gates. From 880bc225686f314c12b4431e72a7ea0d1e05b704 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 8 Mar 2026 23:38:38 +0530 Subject: [PATCH 090/117] Corrected __hash__, __eq__ for Gate meta class Instead of equivalence testing based on namespace, gate name. Testing based on memory location since non-parametrized gates can't be instantiated and parametrized gates have their own __eq__ method. --- pytest.ini | 4 +--- src/qutip_qip/operations/controlled.py | 10 ++++++++- src/qutip_qip/operations/gateclass.py | 30 ++++++++++---------------- src/qutip_qip/operations/parametric.py | 3 +++ 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/pytest.ini b/pytest.ini index 077fbf649..d8be05f29 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,13 +7,11 @@ filterwarnings = error ; ImportWarning: PyxImporter.find_spec() not found ignore:PyxImporter:ImportWarning - ignore::DeprecationWarning:qutip_qip.operations*: + ignore::UserWarning: ignore:matplotlib not found:UserWarning ignore:the imp module is deprecated in favour of importlib:DeprecationWarning ignore:Dedicated options class are no longer needed, options should be passed as dict to solvers.:FutureWarning ignore::DeprecationWarning:qiskit.utils.algorithm_globals: - # Deprecation warning for python = 3.9 with matplotlib 3.9.4 - ignore:'mode' parameter is deprecated # Deprecation warning for scipy disp interface, will be removed in scipy 1.18 ignore:.*`disp` and `iprint` options.*L-BFGS-B.*deprecated.*:DeprecationWarning diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 4210b8109..61101e8e7 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -1,4 +1,5 @@ import inspect +import warnings from typing import Type from functools import partial from abc import abstractmethod @@ -228,11 +229,14 @@ def __eq__(self, other) -> bool: return False # Returns false for CRX(0.5), CRX(0.6) - if self._target_inst != other._target_inst: + if self.is_parametric() and self._target_inst != other._target_inst: return False return True + def __hash__(self) -> int: + return super().__hash__() + def controlled( gate: Type[Gate], @@ -259,6 +263,10 @@ def controlled( (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): diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 7ebd694ad..972b0b749 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -1,7 +1,7 @@ # annotations import won't be needed after minimum version becomes 3.14 (PEP 749) from __future__ import annotations -from abc import ABC, ABCMeta, abstractmethod import inspect +from abc import ABC, ABCMeta, abstractmethod from typing import Type import numpy as np @@ -24,6 +24,11 @@ class _GateMetaClass(ABCMeta): + # __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__ = () + def __init__(cls, name, bases, attrs): """ This method is automatically invoked during class creation. It validates that @@ -92,19 +97,12 @@ def __eq__(cls, other: any) -> bool: if not isinstance(other, type): return False + # 'is' keyword in Python checks check if two variables refer to + # the exact same object in memory, since non-parametrized gate classes + # are not meant to be parametrized it works, ParametrizedGate class + # has its own __eq__ method which acts on an instance. if cls is other: return True - - if isinstance(other, _GateMetaClass): - cls_name = getattr(cls, "name", None) - other_name = getattr(other, "name", None) - - cls_namespace = getattr(cls, "namespace", None) - other_namespace = getattr(other, "namespace", None) - - # They are equal if they share the same name and namespace - return cls_name == other_name and cls_namespace == other_namespace - return False def __hash__(cls) -> int: @@ -113,9 +111,7 @@ def __hash__(cls) -> int: Hashes the class based on its unique identity (namespace and name) so it can still be safely used in the _registry sets and dicts. """ - return hash( - (getattr(cls, "namespace", "std"), getattr(cls, "name", None)) - ) + return id(cls) # By default Python using id() for hashing class Gate(ABC, metaclass=_GateMetaClass): @@ -148,10 +144,6 @@ class attribute for subclasses. 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 diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index cbddd4c12..0576a589b 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -144,6 +144,9 @@ def __eq__(self, other) -> bool: return True + def __hash__(self) -> int: + return super().__hash__() + class AngleParametricGate(ParametricGate): __slots__ = () From 4dcd70e2bbc6a6921ec3d400251c9a81f4d6c9c4 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 8 Mar 2026 23:47:11 +0530 Subject: [PATCH 091/117] Minor fix to inverse in ControlledGate For target_gate where self_inverse is True, return the self itself itself instead of generating a new Controlled version. --- src/qutip_qip/operations/controlled.py | 7 +++++-- src/qutip_qip/operations/gateclass.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 61101e8e7..3719469d7 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -190,8 +190,11 @@ def get_qobj(cls_or_self) -> Qobj: @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 - if isinstance(cls_or_self, type): + elif isinstance(cls_or_self, type): inverse_gate = controlled( cls_or_self.target_gate.inverse(), cls_or_self.num_ctrl_qubits, @@ -265,7 +268,7 @@ def controlled( if found_gate is not None: warnings.warn( f"Found the same existing Controlled Gate {found_gate.name}", - UserWarning + UserWarning, ) return found_gate diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 972b0b749..79be82ed4 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -111,7 +111,7 @@ def __hash__(cls) -> int: Hashes the class based on its unique identity (namespace and name) so it can still be safely used in the _registry sets and dicts. """ - return id(cls) # By default Python using id() for hashing + return id(cls) # By default Python using id() for hashing class Gate(ABC, metaclass=_GateMetaClass): @@ -144,6 +144,7 @@ class attribute for subclasses. The LaTeX string representation of the gate (used for circuit drawing). Defaults to the class name if not provided. """ + __slots__ = () namespace: NameSpace | None = None From 1a8f3d32b394648d7309c09cb0a02d94208522a1 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Sun, 8 Mar 2026 23:56:00 +0530 Subject: [PATCH 092/117] Minor remaining fixes --- src/qutip_qip/circuit/circuit.py | 4 +++- src/qutip_qip/operations/controlled.py | 4 ++-- src/qutip_qip/operations/old_gates.py | 6 ++++++ src/qutip_qip/qiskit/utils/converter.py | 6 +++--- tests/test_qiskit.py | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 2d4472ab5..83fa7e744 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -294,7 +294,9 @@ def add_gate( if control_value is not None: warnings.warn( "'control_value' is no longer a valid argument and has been deprecated and will be removed in the future version. " - "Use gate = controlled(gates.X, num_ctrl_qubits=1, control_value=0) instead", + "from qutip_qip.operations import controlled", + "from qutip_qip.operations.gates import X", + "Example: gate = controlled(X, num_ctrl_qubits=1, control_value=0) instead", DeprecationWarning, stacklevel=2, ) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 3719469d7..db08c09dc 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -148,9 +148,9 @@ def _validate_control_value(cls) -> None: Raises ------ TypeError - If control_value is not an integer. + If ctrl_value is not an integer. ValueError - If control_value is negative or exceeds the maximum value + If ctrl_value is negative or exceeds the maximum value possible for the number of control qubits ($2^N - 1$). """ diff --git a/src/qutip_qip/operations/old_gates.py b/src/qutip_qip/operations/old_gates.py index 8eb16c5a1..097603633 100644 --- a/src/qutip_qip/operations/old_gates.py +++ b/src/qutip_qip/operations/old_gates.py @@ -1026,6 +1026,12 @@ def rotation(op, phi, N=None, target=0): Quantum object for operator describing the rotation. """ + warnings.warn( + "rotation has been deprecated and will be removed in future version.", + DeprecationWarning, + stacklevel=2, + ) + if N is not None: _deprecation_warnings_gate_expansion() return expand_operator(rotation(op, phi), N, target) diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index bcb9b88ac..1725d2734 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -65,13 +65,13 @@ def get_qutip_index(bit_index: int | list, total_bits: int) -> int: Note ---- When we convert a circuit from qiskit to qutip, - the 0st bit is mapped to the 0th bit and (n-1)th bit to (n-q)th bit - and so on. Essentially the bit order stays the same. + the 0st bit is mapped to the (n-1)th bit and 1st bit to (n-2)th bit + and so on. Essentially the bit order is reversed. """ if isinstance(bit_index, Iterable): return [get_qutip_index(bit, total_bits) for bit in bit_index] else: - return bit_index + return total_bits - 1 - bit_index def _get_mapped_bits(bits: list | tuple, bit_map: dict[int, int]) -> list: diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 11cbfb8f2..17a575aa1 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -213,7 +213,7 @@ def test_measurements(self): obtain predetermined results. """ random.seed(1) - predefined_counts = {"0": 233, "11": 267, "10": 270, "1": 254} + predefined_counts = {"0": 233, "11": 267, "1": 270, "10": 254} circ = QuantumCircuit(2, 2) circ.h(0) From 1cf0cf5f11c42bec3d68d56f501dc748ec79f317 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 00:35:20 +0530 Subject: [PATCH 093/117] Fix bug in gate_product, move its logic to operations tensor_list was being undefined previously and U_overalls was set to 1 redundantly, should ideally be set to None. Also the logic for gate_product was split between operation and circuit_simulator module while the API import was via operations module. So moved the entire logic there itself. --- src/qutip_qip/circuit/simulator/__init__.py | 6 - src/qutip_qip/circuit/simulator/utils.py | 240 ------------- src/qutip_qip/operations/utils.py | 354 +++++++++++++++++--- 3 files changed, 300 insertions(+), 300 deletions(-) delete mode 100644 src/qutip_qip/circuit/simulator/utils.py diff --git a/src/qutip_qip/circuit/simulator/__init__.py b/src/qutip_qip/circuit/simulator/__init__.py index 46297004c..d9544da2c 100644 --- a/src/qutip_qip/circuit/simulator/__init__.py +++ b/src/qutip_qip/circuit/simulator/__init__.py @@ -1,13 +1,7 @@ from .result import CircuitResult from .matrix_mul_simulator import CircuitSimulator -from .utils import ( - gate_sequence_product, - gate_sequence_product_with_expansion, -) __all__ = [ "CircuitResult", "CircuitSimulator", - "gate_sequence_product", - "gate_sequence_product_with_expansion", ] diff --git a/src/qutip_qip/circuit/simulator/utils.py b/src/qutip_qip/circuit/simulator/utils.py deleted file mode 100644 index 4f52eb8a2..000000000 --- a/src/qutip_qip/circuit/simulator/utils.py +++ /dev/null @@ -1,240 +0,0 @@ -from itertools import chain -from qutip import tensor -from qutip_qip.operations import expand_operator - - -def _flatten(lst): - """ - Helper to flatten lists. - """ - - return [item for sublist in lst for item in sublist] - - -def _mult_sublists(tensor_list, overall_inds, U, inds): - """ - Calculate the revised indices and tensor list by multiplying a new unitary - U applied to inds. - - Parameters - ---------- - tensor_list : list of Qobj - List of gates (unitaries) acting on disjoint qubits. - - overall_inds : list of list of int - List of qubit indices corresponding to each gate in tensor_list. - - U: Qobj - Unitary to be multiplied with the the unitary specified by tensor_list. - - inds: list of int - List of qubit indices corresponding to U. - - Returns - ------- - tensor_list_revised: list of Qobj - List of gates (unitaries) acting on disjoint qubits incorporating U. - - overall_inds_revised: list of list of int - List of qubit indices corresponding to each gate in tensor_list_revised. - - Examples - -------- - - First, we get some imports out of the way, - - >>> from qutip_qip.operations.gates import _mult_sublists - >>> from qutip_qip.operations.gates import X, Y, Z - - Suppose we have a unitary list of already processed gates, - X, Y, Z applied on qubit indices 0, 1, 2 respectively and - encounter a new TOFFOLI gate on qubit indices (0, 1, 3). - - >>> tensor_list = [X.get_qobj(), Y.get_qobj(), Z.get_qobj()] - >>> overall_inds = [[0], [1], [2]] - >>> U = toffoli() - >>> U_inds = [0, 1, 3] - - Then, we can use _mult_sublists to produce a new list of unitaries by - multiplying TOFFOLI (and expanding) only on the qubit indices involving - TOFFOLI gate (and any multiplied gates). - - >>> U_list, overall_inds = _mult_sublists(tensor_list, overall_inds, U, U_inds) - >>> np.testing.assert_allclose(U_list[0]) == Z.get_qobj()) - >>> toffoli_xy = toffoli() * tensor(X.get_qobj(), Y.get_qobj(), identity(2)) - >>> np.testing.assert_allclose(U_list[1]), toffoli_xy) - >>> overall_inds = [[2], [0, 1, 3]] - """ - - tensor_sublist = [] - inds_sublist = [] - - tensor_list_revised = [] - overall_inds_revised = [] - - for sub_inds, sub_U in zip(overall_inds, tensor_list): - if len(set(sub_inds).intersection(inds)) > 0: - tensor_sublist.append(sub_U) - inds_sublist.append(sub_inds) - else: - overall_inds_revised.append(sub_inds) - tensor_list_revised.append(sub_U) - - inds_sublist = _flatten(inds_sublist) - U_sublist = tensor(tensor_sublist) - - revised_inds = list(set(inds_sublist).union(set(inds))) - N = len(revised_inds) - - sorted_positions = sorted(range(N), key=lambda key: revised_inds[key]) - ind_map = {ind: pos for ind, pos in zip(revised_inds, sorted_positions)} - - U_sublist = expand_operator( - U_sublist, dims=[2] * N, targets=[ind_map[ind] for ind in inds_sublist] - ) - U = expand_operator( - U, dims=[2] * N, targets=[ind_map[ind] for ind in inds] - ) - - U_sublist = U * U_sublist - inds_sublist = revised_inds - - overall_inds_revised.append(inds_sublist) - tensor_list_revised.append(U_sublist) - - return tensor_list_revised, overall_inds_revised - - -def _expand_overall(tensor_list, overall_inds): - """ - Tensor unitaries in tensor list and then use expand_operator to rearrange - them appropriately according to the indices in overall_inds. - """ - - U_overall = tensor(tensor_list) - overall_inds = _flatten(overall_inds) - U_overall = expand_operator( - U_overall, dims=[2] * len(overall_inds), targets=overall_inds - ) - overall_inds = sorted(overall_inds) - return U_overall, overall_inds - - -def gate_sequence_product(U_list, ind_list): - """ - Calculate the overall unitary matrix for a given list of unitary operations - that are still of original dimension. - - Parameters - ---------- - U_list : list of Qobj - List of gates(unitaries) implementing the quantum circuit. - - ind_list : list of list of int - List of qubit indices corresponding to each gate in tensor_list. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - - overall_inds : list of int - List of qubit indices on which U_overall applies. - - Examples - -------- - - First, we get some imports out of the way, - - >>> from qutip_qip.operations.gates import gate_sequence_product - >>> from qutip_qip.operations.gates import X, Y, Z, TOFFOLI - - Suppose we have a circuit with gates X, Y, Z, TOFFOLI - applied on qubit indices 0, 1, 2 and [0, 1, 3] respectively. - - >>> tensor_lst = [X.get_qobj(), Y.get_qobj(), Z.get_qobj(), TOFFOLI.get_qobj()] - >>> overall_inds = [[0], [1], [2], [0, 1, 3]] - - Then, we can use gate_sequence_product to produce a single unitary - obtained by multiplying unitaries in the list using heuristic methods - to reduce the size of matrices being multiplied. - - >>> U_list, overall_inds = gate_sequence_product(tensor_lst, overall_inds) - """ - num_qubits = len(set(chain(*ind_list))) - sorted_inds = sorted(set(_flatten(ind_list))) - ind_list = [[sorted_inds.index(ind) for ind in inds] for inds in ind_list] - - U_overall = 1 - overall_inds = [] - - for i, (U, inds) in enumerate(zip(U_list, ind_list)): - # when the tensor_list covers the full dimension of the circuit, we - # expand the tensor_list to a unitary and call gate_sequence_product - # recursively on the rest of the U_list. - if len(overall_inds) == 1 and len(overall_inds[0]) == num_qubits: - # FIXME undefined variable tensor_list - U_overall, overall_inds = _expand_overall( - tensor_list, overall_inds - ) - U_left, rem_inds = gate_sequence_product(U_list[i:], ind_list[i:]) - U_left = expand_operator( - U_left, dims=[2] * num_qubits, targets=rem_inds - ) - return U_left * U_overall, [ - sorted_inds[ind] for ind in overall_inds - ] - - # special case for first unitary in the list - if U_overall == 1: - U_overall = U_overall * U - overall_inds = [ind_list[0]] - tensor_list = [U_overall] - continue - - # case where the next unitary interacts on some subset of qubits - # with the unitaries already in tensor_list. - elif len(set(_flatten(overall_inds)).intersection(set(inds))) > 0: - tensor_list, overall_inds = _mult_sublists( - tensor_list, overall_inds, U, inds - ) - - # case where the next unitary does not interact with any unitary in - # tensor_list - else: - overall_inds.append(inds) - tensor_list.append(U) - - U_overall, overall_inds = _expand_overall(tensor_list, overall_inds) - - return U_overall, [sorted_inds[ind] for ind in overall_inds] - - -def gate_sequence_product_with_expansion(U_list, left_to_right=True): - """ - Calculate the overall unitary matrix for a given list of unitary - operations, assuming that all operations have the same dimension. - This is only for backward compatibility. - - Parameters - ---------- - U_list : list - List of gates(unitaries) implementing the quantum circuit. - - left_to_right : Boolean - Check if multiplication is to be done from left to right. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - """ - - U_overall = 1 - for U in U_list: - if left_to_right: - U_overall = U * U_overall - else: - U_overall = U_overall * U - - return U_overall diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index eb9ca7e0e..47034a95e 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -1,10 +1,55 @@ -import numbers from collections.abc import Iterable +from itertools import chain +import numbers +from qutip import Qobj, identity, tensor +from scipy.linalg import block_diag import numpy as np import qutip -from scipy.linalg import block_diag -from qutip import Qobj, identity, tensor + + +def controlled_gate_unitary( + U: Qobj, + num_controls: int, + control_value: int, +) -> Qobj: + """ + Create an N-qubit controlled gate from a single-qubit gate U with the given + control and target qubits. + + Parameters + ---------- + U : :class:`qutip.Qobj` + An arbitrary unitary gate. + controls : list of int + The index of the first control qubit. + targets : list of int + The index of the target qubit. + N : int + The total number of qubits. + control_value : int + The decimal value of the controlled qubits that activates the gate U. + + Returns + ------- + result : qobj + Quantum object representing the controlled-U gate. + """ + # Compatibility + num_targets = len(U.dims[0]) + + # First, assume that the last qubit is the target and control qubits are + # in the increasing order. + # The control_value is the location of this unitary. + target_dim = U.shape[0] + block_matrices = [np.eye(target_dim) for _ in range(2**num_controls)] + block_matrices[control_value] = U.full() + + result = block_diag(*block_matrices) + result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) + + # Expand it to N qubits and permute qubits labelling + return result def _check_oper_dims( @@ -198,6 +243,256 @@ def hadamard_transform(N=1): return tensor([H] * N) +def _flatten(lst): + """ + Helper to flatten lists. + """ + + return [item for sublist in lst for item in sublist] + + +def _mult_sublists(tensor_list, overall_inds, U, inds): + """ + Calculate the revised indices and tensor list by multiplying a new unitary + U applied to inds. + + Parameters + ---------- + tensor_list : list of Qobj + List of gates (unitaries) acting on disjoint qubits. + + overall_inds : list of list of int + List of qubit indices corresponding to each gate in tensor_list. + + U: Qobj + Unitary to be multiplied with the the unitary specified by tensor_list. + + inds: list of int + List of qubit indices corresponding to U. + + Returns + ------- + tensor_list_revised: list of Qobj + List of gates (unitaries) acting on disjoint qubits incorporating U. + + overall_inds_revised: list of list of int + List of qubit indices corresponding to each gate in tensor_list_revised. + + Examples + -------- + + First, we get some imports out of the way, + + >>> from qutip_qip.operations.gates import _mult_sublists + >>> from qutip_qip.operations.gates import X, Y, Z + + Suppose we have a unitary list of already processed gates, + X, Y, Z applied on qubit indices 0, 1, 2 respectively and + encounter a new TOFFOLI gate on qubit indices (0, 1, 3). + + >>> tensor_list = [X.get_qobj(), Y.get_qobj(), Z.get_qobj()] + >>> overall_inds = [[0], [1], [2]] + >>> U = toffoli() + >>> U_inds = [0, 1, 3] + + Then, we can use _mult_sublists to produce a new list of unitaries by + multiplying TOFFOLI (and expanding) only on the qubit indices involving + TOFFOLI gate (and any multiplied gates). + + >>> U_list, overall_inds = _mult_sublists(tensor_list, overall_inds, U, U_inds) + >>> np.testing.assert_allclose(U_list[0]) == Z.get_qobj()) + >>> toffoli_xy = toffoli() * tensor(X.get_qobj(), Y.get_qobj(), identity(2)) + >>> np.testing.assert_allclose(U_list[1]), toffoli_xy) + >>> overall_inds = [[2], [0, 1, 3]] + """ + + tensor_sublist = [] + inds_sublist = [] + + tensor_list_revised = [] + overall_inds_revised = [] + + for sub_inds, sub_U in zip(overall_inds, tensor_list): + if len(set(sub_inds).intersection(inds)) > 0: + tensor_sublist.append(sub_U) + inds_sublist.append(sub_inds) + else: + overall_inds_revised.append(sub_inds) + tensor_list_revised.append(sub_U) + + inds_sublist = _flatten(inds_sublist) + U_sublist = tensor(tensor_sublist) + + revised_inds = list(set(inds_sublist).union(set(inds))) + N = len(revised_inds) + + sorted_positions = sorted(range(N), key=lambda key: revised_inds[key]) + ind_map = {ind: pos for ind, pos in zip(revised_inds, sorted_positions)} + + U_sublist = expand_operator( + U_sublist, dims=[2] * N, targets=[ind_map[ind] for ind in inds_sublist] + ) + U = expand_operator( + U, dims=[2] * N, targets=[ind_map[ind] for ind in inds] + ) + + U_sublist = U * U_sublist + inds_sublist = revised_inds + + overall_inds_revised.append(inds_sublist) + tensor_list_revised.append(U_sublist) + + return tensor_list_revised, overall_inds_revised + + +def _expand_overall(tensor_list, overall_inds): + """ + Tensor unitaries in tensor list and then use expand_operator to rearrange + them appropriately according to the indices in overall_inds. + """ + + U_overall = tensor(tensor_list) + overall_inds = _flatten(overall_inds) + + # Map indices to a contiguous 0...N-1 range to prevent out-of-bounds in expand_operator + N = len(overall_inds) + sorted_positions = sorted(range(N), key=lambda key: overall_inds[key]) + ind_map = {ind: pos for ind, pos in zip(overall_inds, sorted_positions)} + mapped_targets = [ind_map[ind] for ind in overall_inds] + + U_overall = expand_operator( + U_overall, dims=[2] * len(overall_inds), targets=mapped_targets + ) + overall_inds = sorted(overall_inds) + return U_overall, overall_inds + + +def _gate_sequence_product(U_list, ind_list): + """ + Calculate the overall unitary matrix for a given list of unitary operations + that are still of original dimension. + + Parameters + ---------- + U_list : list of Qobj + List of gates(unitaries) implementing the quantum circuit. + + ind_list : list of list of int + List of qubit indices corresponding to each gate in tensor_list. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + + overall_inds : list of int + List of qubit indices on which U_overall applies. + + Examples + -------- + + First, we get some imports out of the way, + + >>> from qutip_qip.operations.gates import gate_sequence_product + >>> from qutip_qip.operations.gates import X, Y, Z, TOFFOLI + + Suppose we have a circuit with gates X, Y, Z, TOFFOLI + applied on qubit indices 0, 1, 2 and [0, 1, 3] respectively. + + >>> tensor_lst = [X.get_qobj(), Y.get_qobj(), Z.get_qobj(), TOFFOLI.get_qobj()] + >>> overall_inds = [[0], [1], [2], [0, 1, 3]] + + Then, we can use gate_sequence_product to produce a single unitary + obtained by multiplying unitaries in the list using heuristic methods + to reduce the size of matrices being multiplied. + + >>> U_list, overall_inds = gate_sequence_product(tensor_lst, overall_inds) + """ + if not U_list: + return None, [] + + num_qubits = len(set(chain(*ind_list))) + sorted_inds = sorted(set(_flatten(ind_list))) + ind_list = [[sorted_inds.index(ind) for ind in inds] for inds in ind_list] + + U_overall = None + overall_inds = [] + tensor_list = [] + + for i, (U, inds) in enumerate(zip(U_list, ind_list)): + # when the tensor_list covers the full dimension of the circuit, we + # expand the tensor_list to a unitary and call gate_sequence_product + # recursively on the rest of the U_list. + if len(overall_inds) == 1 and len(overall_inds[0]) == num_qubits: + # FIXME undefined variable tensor_list + U_overall, overall_inds = _expand_overall( + tensor_list, overall_inds + ) + U_left, rem_inds = _gate_sequence_product(U_list[i:], ind_list[i:]) + U_left = expand_operator( + U_left, dims=[2] * num_qubits, targets=rem_inds + ) + return U_left * U_overall, [ + sorted_inds[ind] for ind in overall_inds + ] + + if U_overall is None: + U_overall = U + overall_inds = [ind_list[0]] + tensor_list = [U_overall] + continue + + # case where the next unitary interacts on some subset of qubits + # with the unitaries already in tensor_list. + elif len(set(_flatten(overall_inds)).intersection(set(inds))) > 0: + tensor_list, overall_inds = _mult_sublists( + tensor_list, overall_inds, U, inds + ) + + # case where the next unitary does not interact with any unitary in + # tensor_list + else: + overall_inds.append(inds) + tensor_list.append(U) + + U_overall, overall_inds = _expand_overall(tensor_list, overall_inds) + + return U_overall, [sorted_inds[ind] for ind in overall_inds] + + +def _gate_sequence_product_with_expansion(U_list, left_to_right=True): + """ + Calculate the overall unitary matrix for a given list of unitary + operations, assuming that all operations have the same dimension. + This is only for backward compatibility. + + Parameters + ---------- + U_list : list + List of gates(unitaries) implementing the quantum circuit. + + left_to_right : Boolean + Check if multiplication is to be done from left to right. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + """ + + if len(U_list) == 0: + raise ValueError("Got an empty U_list") + + U_overall = U_list[0] + for U in U_list[1:]: + if left_to_right: + U_overall = U * U_overall + else: + U_overall = U_overall * U + + return U_overall + + def gate_sequence_product( U_list: list[Qobj], left_to_right: bool = True, @@ -230,56 +525,7 @@ def gate_sequence_product( overall_inds : list of int, optional List of qubit indices on which U_overall applies. """ - from qutip_qip.circuit.simulator import ( - gate_sequence_product, - gate_sequence_product_with_expansion, - ) - if expand: - return gate_sequence_product(U_list, inds_list) + return _gate_sequence_product(U_list, inds_list) else: - return gate_sequence_product_with_expansion(U_list, left_to_right) - - -def controlled_gate_unitary( - U: Qobj, - num_controls: int, - control_value: int, -) -> Qobj: - """ - Create an N-qubit controlled gate from a single-qubit gate U with the given - control and target qubits. - - Parameters - ---------- - U : :class:`qutip.Qobj` - An arbitrary unitary gate. - controls : list of int - The index of the first control qubit. - targets : list of int - The index of the target qubit. - N : int - The total number of qubits. - control_value : int - The decimal value of the controlled qubits that activates the gate U. - - Returns - ------- - result : qobj - Quantum object representing the controlled-U gate. - """ - # Compatibility - num_targets = len(U.dims[0]) - - # First, assume that the last qubit is the target and control qubits are - # in the increasing order. - # The control_value is the location of this unitary. - target_dim = U.shape[0] - block_matrices = [np.eye(target_dim) for _ in range(2**num_controls)] - block_matrices[control_value] = U.full() - - result = block_diag(*block_matrices) - result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) - - # Expand it to N qubits and permute qubits labelling - return result + return _gate_sequence_product_with_expansion(U_list, left_to_right) From d63d2db2e95617865adbbd6a60f48ebcab46248d Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 02:23:17 +0530 Subject: [PATCH 094/117] Added test gates for which gates must raise an Error --- src/qutip_qip/operations/namespace.py | 2 +- tests/test_circuit.py | 82 +++++++- tests/test_gates.py | 273 +++++++++++++++++++------- 3 files changed, 279 insertions(+), 78 deletions(-) diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index e07623126..9bcd56abb 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -68,7 +68,7 @@ def register( """ if name in self._registry: raise NameError( - f"'{operation_cls.name}' already exists in namespace '{self.name}'" + f"'{operation_cls.__name__}' already exists in namespace '{self.name}'" ) self._registry[name] = operation_cls diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 77221195f..b07b67845 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -5,7 +5,7 @@ from pathlib import Path -import qutip as qp +import qutip from qutip import ( tensor, Qobj, @@ -439,7 +439,7 @@ def test_N_level_system(self): """ Test for circuit with N-level system. """ - mat3 = qp.rand_unitary(3) + mat3 = qutip.rand_unitary(3) class CTRLMAT3(Gate): num_qubits = 2 @@ -461,12 +461,12 @@ def get_qobj(): qc = QubitCircuit(2, dims=[3, 2]) qc.add_gate(CTRLMAT3, targets=[1, 0]) props = qc.propagators() - final_fid = qp.average_gate_fidelity(mat3, ptrace(props[0], 0) - 1) + final_fid = qutip.average_gate_fidelity(mat3, ptrace(props[0], 0) - 1) assert pytest.approx(final_fid, 1.0e-6) == 1 init_state = basis([3, 2], [0, 1]) result = qc.run(init_state) - final_fid = qp.fidelity(result, props[0] * init_state) + final_fid = qutip.fidelity(result, props[0] * init_state) assert pytest.approx(final_fid, 1.0e-6) == 1.0 @pytest.mark.repeat(10) @@ -504,7 +504,7 @@ def test_classical_control(self): classical_control_value=1, ) result = qc.run(basis(2, 0), cbits=[1, 0]) - fid = qp.fidelity(result, basis(2, 0)) + fid = qutip.fidelity(result, basis(2, 0)) assert pytest.approx(fid, 1.0e-6) == 1 qc = QubitCircuit(1, num_cbits=2) @@ -515,7 +515,7 @@ def test_classical_control(self): classical_control_value=2, ) result = qc.run(basis(2, 0), cbits=[1, 0]) - fid = qp.fidelity(result, basis(2, 1)) + fid = qutip.fidelity(result, basis(2, 1)) assert pytest.approx(fid, 1.0e-6) == 1 def test_runstatistics_teleportation(self): @@ -581,12 +581,12 @@ def test_circuit_with_selected_measurement_result(self): # if we don's select the measurement result, # the two circuit should return the same value. np.random.seed(0) - final_state = qc.run(qp.basis(2, 0), cbits=[0], measure_results=[0]) - fid = pytest.approx(qp.fidelity(final_state, basis(2, 0))) + final_state = qc.run(qutip.basis(2, 0), cbits=[0], measure_results=[0]) + fid = pytest.approx(qutip.fidelity(final_state, basis(2, 0))) assert fid == 1.0 np.random.seed(0) - final_state = qc.run(qp.basis(2, 0), cbits=[0], measure_results=[1]) - fid = pytest.approx(qp.fidelity(final_state, basis(2, 1))) + final_state = qc.run(qutip.basis(2, 0), cbits=[0], measure_results=[1]) + fid = pytest.approx(qutip.fidelity(final_state, basis(2, 1))) assert fid == 1.0 def test_gate_product(self): @@ -769,3 +769,65 @@ def test_circuit_chain_structure(self): assert qc2.reverse_states is True assert qc2.input_states == [None] * 3 + + +def test_gates_class(): + init_state = qutip.rand_ket([2, 2, 2]) + + circuit1 = QubitCircuit(3) + circuit1.add_gate(gates.X, targets=1) + circuit1.add_gate(gates.Y, targets=1) + circuit1.add_gate(gates.Z, targets=2) + circuit1.add_gate(gates.RX(np.pi / 4), targets=0) + circuit1.add_gate(gates.RY(np.pi / 4), targets=0) + circuit1.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit1.add_gate(gates.H, targets=0) + circuit1.add_gate(gates.SQRTX, targets=0) + circuit1.add_gate(gates.S, targets=2) + circuit1.add_gate(gates.T, targets=1) + circuit1.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit1.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) + circuit1.add_gate(gates.CX, controls=0, targets=1) + circuit1.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) + circuit1.add_gate(gates.SWAP, targets=[0, 1]) + circuit1.add_gate(gates.ISWAP, targets=[2, 1]) + circuit1.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit1.add_gate(gates.SQRTSWAP, [2, 0]) + circuit1.add_gate(gates.SQRTISWAP, [0, 1]) + circuit1.add_gate(gates.SWAPALPHA(np.pi / 4), [1, 2]) + circuit1.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) + circuit1.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit1.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit1.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit1.add_gate(gates.RZX(1.0), targets=[1, 0]) + result1 = circuit1.run(init_state) + + circuit2 = QubitCircuit(3) + circuit2.add_gate(gates.X, targets=1) + circuit2.add_gate(gates.Y, targets=1) + circuit2.add_gate(gates.Z, targets=2) + circuit2.add_gate(gates.RX(np.pi / 4), targets=0) + circuit2.add_gate(gates.RY(np.pi / 4), targets=0) + circuit2.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit2.add_gate(gates.H, targets=0) + circuit2.add_gate(gates.SQRTX, targets=0) + circuit2.add_gate(gates.S, targets=2) + circuit2.add_gate(gates.T, targets=1) + circuit2.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit2.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) + circuit2.add_gate(gates.CX, controls=0, targets=1) + circuit2.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) + circuit2.add_gate(gates.SWAP, targets=[0, 1]) + circuit2.add_gate(gates.ISWAP, targets=[2, 1]) + circuit2.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit2.add_gate(gates.SQRTSWAP, targets=[2, 0]) + circuit2.add_gate(gates.SQRTISWAP, targets=[0, 1]) + circuit2.add_gate(gates.SWAPALPHA(np.pi / 4), targets=[1, 2]) + circuit2.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) + circuit2.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit2.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit2.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit2.add_gate(gates.RZX(1.0), targets=[1, 0]) + result2 = circuit2.run(init_state) + + assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 diff --git a/tests/test_gates.py b/tests/test_gates.py index 1595a93ec..c18697630 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -1,17 +1,21 @@ from copy import deepcopy +from typing import Type import pytest import itertools -from typing import Type + import numpy as np import qutip -from qutip_qip.circuit import QubitCircuit + from qutip_qip.operations import ( + AngleParametricGate, Gate, - expand_operator, + ParametricGate, + NameSpace, controlled, + expand_operator, get_unitary_gate, - qubit_clifford_group, hadamard_transform, + qubit_clifford_group, ) import qutip_qip.operations.gates as gates @@ -269,7 +273,8 @@ def test_two_qubit(self, gate, n_controls): ], ) def test_three_qubit(self, gate: Type[Gate], n_controls): - targets = [qutip.rand_ket(2) for _ in [None] * 3] + targets = [qutip.rand_ket(2) for _ in [ +None] * 3] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(targets) @@ -396,68 +401,6 @@ def test_dtype(self): assert isinstance(expanded_qobj, qutip.data.Dense) -def test_gates_class(): - init_state = qutip.rand_ket([2, 2, 2]) - - circuit1 = QubitCircuit(3) - circuit1.add_gate(gates.X, targets=1) - circuit1.add_gate(gates.Y, targets=1) - circuit1.add_gate(gates.Z, targets=2) - circuit1.add_gate(gates.RX(np.pi / 4), targets=0) - circuit1.add_gate(gates.RY(np.pi / 4), targets=0) - circuit1.add_gate(gates.RZ(np.pi / 4), targets=1) - circuit1.add_gate(gates.H, targets=0) - circuit1.add_gate(gates.SQRTX, targets=0) - circuit1.add_gate(gates.S, targets=2) - circuit1.add_gate(gates.T, targets=1) - circuit1.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) - circuit1.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) - circuit1.add_gate(gates.CX, controls=0, targets=1) - circuit1.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) - circuit1.add_gate(gates.SWAP, targets=[0, 1]) - circuit1.add_gate(gates.ISWAP, targets=[2, 1]) - circuit1.add_gate(gates.CZ, controls=[0], targets=[2]) - circuit1.add_gate(gates.SQRTSWAP, [2, 0]) - circuit1.add_gate(gates.SQRTISWAP, [0, 1]) - circuit1.add_gate(gates.SWAPALPHA(np.pi / 4), [1, 2]) - circuit1.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) - circuit1.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) - circuit1.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) - circuit1.add_gate(gates.BERKELEY, targets=[1, 0]) - circuit1.add_gate(gates.RZX(1.0), targets=[1, 0]) - result1 = circuit1.run(init_state) - - circuit2 = QubitCircuit(3) - circuit2.add_gate(gates.X, targets=1) - circuit2.add_gate(gates.Y, targets=1) - circuit2.add_gate(gates.Z, targets=2) - circuit2.add_gate(gates.RX(np.pi / 4), targets=0) - circuit2.add_gate(gates.RY(np.pi / 4), targets=0) - circuit2.add_gate(gates.RZ(np.pi / 4), targets=1) - circuit2.add_gate(gates.H, targets=0) - circuit2.add_gate(gates.SQRTX, targets=0) - circuit2.add_gate(gates.S, targets=2) - circuit2.add_gate(gates.T, targets=1) - circuit2.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) - circuit2.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) - circuit2.add_gate(gates.CX, controls=0, targets=1) - circuit2.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) - circuit2.add_gate(gates.SWAP, targets=[0, 1]) - circuit2.add_gate(gates.ISWAP, targets=[2, 1]) - circuit2.add_gate(gates.CZ, controls=[0], targets=[2]) - circuit2.add_gate(gates.SQRTSWAP, targets=[2, 0]) - circuit2.add_gate(gates.SQRTISWAP, targets=[0, 1]) - circuit2.add_gate(gates.SWAPALPHA(np.pi / 4), targets=[1, 2]) - circuit2.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) - circuit2.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) - circuit2.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) - circuit2.add_gate(gates.BERKELEY, targets=[1, 0]) - circuit2.add_gate(gates.RZX(1.0), targets=[1, 0]) - result2 = circuit2.run(init_state) - - assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 - - GATES = [ gates.X, gates.Y, @@ -519,3 +462,199 @@ def test_gate_inverse(gate: Gate | Type[Gate]): np.eye(n), atol=1e-12, ) + + +class TestOperationsErrors: + def test_gateclass_errors(self): + with pytest.raises(TypeError): + class BadGate(Gate): + num_qubits = -1 + + def get_qobj(cls): pass + + with pytest.raises(TypeError): + class BadGate2(Gate): + num_qubits = 1 + is_clifford = 1 # attribute 'is_clifford' must be a bool + + def get_qobj(cls): pass + + with pytest.raises(TypeError): + class BadGate3(Gate): + num_qubits = 1 + self_inverse = 1 # attribute 'self_inverse' must be a bool + + def get_qobj(cls): pass + + with pytest.raises(TypeError): + class BadGate4(Gate): + num_qubits = 1 + self_inverse = True + + def get_qobj(cls): pass + def inverse(cls): pass # Can't define inverse method is self_inverse=True + + class GoodGate(Gate): + num_qubits = 1 + @staticmethod + def get_qobj(): pass + + with pytest.raises(AttributeError): + GoodGate.num_qubits = 2 + # For a given gateclass, class attribute like num_qubit can't be modified + + with pytest.raises(NotImplementedError): + GoodGate.inverse() + + U_rect = qutip.Qobj(np.eye(2, 3)) + with pytest.raises(ValueError): + get_unitary_gate("U_rect", U_rect) # U must be a square matrix + + U_dim = qutip.Qobj(np.eye(3)) + with pytest.raises(ValueError): + get_unitary_gate("U_dim", U_dim) # 3 != 2^n + + U_not_unitary = qutip.Qobj(np.zeros((2, 2))) + with pytest.raises(ValueError): + get_unitary_gate("U_not_unitary", U_not_unitary) # U must be unitaru + + def test_parametric_gate_errors(self): + with pytest.raises(TypeError): + class BadParamGate(ParametricGate): + num_params = -1 + @staticmethod + def _compute_qobj(args): pass + @staticmethod + def validate_params(args): pass + + with pytest.raises(TypeError): + class BadParamGate2(ParametricGate): + num_params = 1.5 + @staticmethod + def _compute_qobj(args): pass + @staticmethod + def validate_params(args): pass + + class GoodParamGate(AngleParametricGate): + num_qubits = 1 + num_params = 2 + @staticmethod + def _compute_qobj(args): return qutip.qeye(2) + + with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): + GoodParamGate(1.0) + + with pytest.raises(ValueError): + GoodParamGate(1.0, "wrong") # second argument is a string instead of float + + gate = GoodParamGate(1.0, 2.0) + with pytest.raises(NotImplementedError): + gate.inverse() + + def test_controlled_gate_errors(self): + from qutip_qip.operations.controlled import ControlledGate + import qutip_qip.operations.gates as gates + + with pytest.raises(TypeError): + class BadCtrlGate(ControlledGate): + target_gate = gates.RX(0) # target_gate must be a gate subclass not an instantiated onject + num_ctrl_qubits = 1 + ctrl_value = 1 + num_qubits = 2 + + with pytest.raises(TypeError): + class BadCtrlGate2(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = -1 # Can't be negative + ctrl_value = 1 + num_qubits = 2 + + with pytest.raises(ValueError): + class BadCtrlGate3(ControlledGate): + target_gate = gates.X + num_qubits = 2 + num_ctrl_qubits = 2 # Must be less than num_qubit + ctrl_value = 1 + + with pytest.raises(AttributeError): + class BadCtrlGate4(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = 1 # No of control qubits must be 2, since target gate is X + ctrl_value = 1 + num_qubits = 3 + + with pytest.raises(TypeError): + class BadCtrlGate5(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = 1 + ctrl_value = 1.5 # Control value must be an int + num_qubits = 2 + + with pytest.raises(ValueError): + class BadCtrlGate6(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = 1 + ctrl_value = 2 # Control value can't be greater than 1 in this case + num_qubits = 2 + + def test_namespace_errors(self): + with pytest.raises(ValueError): + NameSpace("bad.namespace") # namespace cannot contain dots + + ns1 = NameSpace("test_ns") + with pytest.raises(ValueError): + _ = NameSpace("test_ns") # Existing namespace + + class TmpGate(Gate): + num_qubits = 1 + + ns1.register("tmp_gate", TmpGate) + with pytest.raises(NameError, match="already exists in namespace"): + ns1.register("tmp_gate", TmpGate) + + def test_utils_errors(self): + from qutip_qip.operations.utils import ( + _check_oper_dims, _targets_to_list, expand_operator, gate_sequence_product + ) + + with pytest.raises(ValueError): + _check_oper_dims(qutip.basis(2, 0)) # The operator is not an Qobj with the same input and output dimensions. + + op = qutip.qeye(2) + with pytest.raises(ValueError): + _check_oper_dims(oper=op, dims=[3], targets=[0]) # The dims don't match + + with pytest.raises(TypeError): + _targets_to_list(1.5, op, 1) # targets should be an integer or a list of integer + + with pytest.raises(ValueError): + expand_operator(op, 2, 0) # dims needs to be an interable + + with pytest.raises(ValueError): + gate_sequence_product([], left_to_right=True) # empty U_list + + def test_standard_gates_failures(self): + # Instantiation on non-parametric + with pytest.raises(TypeError): + gates.CX() + + # Wrong no. of parameters + with pytest.raises(TypeError): + gates.CRX(0, 1, 2) + + # Wrong no. of arguments on parametric + with pytest.raises(TypeError): + gates.CRX.get_qobj() + +def test_gate_equality(): + assert gates.CX == gates.CX + assert gates.CX != gates.CY + assert gates.CX != gates.CRX(0.5) + + assert gates.RX(0.5) == gates.RX(0.5) + assert gates.RX(0.5) != gates.RY(0.5) + assert gates.RX(0.5) != gates.RX(0.6) + + assert gates.CRX(0.5) == gates.CRX(0.5) + assert gates.CRX(0.5) != gates.CRY(0.5) + assert gates.CRX(0.5) != gates.CRX(0.6) From f718e9c3799ce5aadd715a58e7c78f02dcdafbad Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 03:35:38 +0530 Subject: [PATCH 095/117] Added more checks to __init_subclass__ in Gate class --- src/qutip_qip/operations/controlled.py | 2 +- src/qutip_qip/operations/gateclass.py | 40 ++++++++++++++++--- src/qutip_qip/operations/gates/other_gates.py | 4 +- .../operations/gates/two_qubit_gate.py | 1 - src/qutip_qip/operations/parametric.py | 25 ++++++++++-- src/qutip_qip/qasm.py | 1 - 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index db08c09dc..4c336b73f 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -133,7 +133,7 @@ def __setattr__(self, name, value) -> None: 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!) + # 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 diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 79be82ed4..8807d2caf 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -24,11 +24,6 @@ class _GateMetaClass(ABCMeta): - # __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__ = () - def __init__(cls, name, bases, attrs): """ This method is automatically invoked during class creation. It validates that @@ -59,7 +54,7 @@ def __init__(cls, name, bases, attrs): # For lookup dictionary for Controlled Gates # e.g. controlled(X, num_ctrl_qubits=1, ctrl_value=1) this is CX, # why do we have to define it again if it already exists. - if getattr(cls, "namespace", None) is not None and cls.is_controlled(): + if cls.is_controlled() and getattr(cls, "namespace", None) is not None: cls.namespace.register( (cls.target_gate.name, cls.num_ctrl_qubits, cls.ctrl_value), cls, @@ -145,6 +140,9 @@ class attribute for subclasses. 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 @@ -218,6 +216,36 @@ def __init_subclass__(cls, **kwargs): f"Remove the method; the base class handles it automatically." ) + try: + param_flag = cls.is_parametric() + except TypeError as e: + raise TypeError( + f"Class '{cls.name}' must define 'is_parametric()' as a callable " + f"@staticmethod or @classmethod taking no instance arguments. " + f"Error: {e}" + ) + + if type(param_flag) is not bool: + raise TypeError( + f"Class '{cls.name}' method 'is_controlled()' must return a strict bool, " + f"got {type(param_flag)} with value {param_flag}." + ) + + try: + control_flag = cls.is_controlled() + except TypeError as e: + raise TypeError( + f"Class '{cls.name}' must define 'is_parametric()' as a callable " + f"@staticmethod or @classmethod taking no instance arguments. " + f"Error: {e}" + ) + + if type(control_flag) is not bool: + raise TypeError( + f"Class '{cls.name}' method 'is_controlled()' must return a strict bool, " + f"got {type(control_flag)} with value {control_flag}." + ) + def __init__(self) -> None: """ This method is overwritten by Parametrized and Controlled Gates. diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index f816c84cb..d8428cf2d 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -19,7 +19,7 @@ class GLOBALPHASE(AngleParametricGate): >>> from qutip_qip.operations.gates import GLOBALPHASE """ - __slots__ = "phase" + __slots__ = () namespace = NS_GATE num_qubits: Final[int] = 0 @@ -34,7 +34,7 @@ def __repr__(self): return f"Gate({self.name}, phase {self.arg_value[0]})" def _compute_qobj(): - pass + raise NotImplementedError def get_qobj(self, num_qubits=None): phase = self.arg_value[0] diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index df2743ac7..131412cd7 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -1,6 +1,5 @@ from typing import Final, Type from functools import cache, lru_cache -import warnings import numpy as np from qutip import Qobj diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 0576a589b..2947b8131 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -4,6 +4,7 @@ from qutip import Qobj from qutip_qip.operations import Gate +from qutip_qip.typing import Real class ParametricGate(Gate): @@ -63,6 +64,25 @@ def __init_subclass__(cls, **kwargs) -> None: f"got {type(num_params)} with value {num_params}." ) + # Validate params must take only one arguments args + # Inspect doesn't count self/cls as another argument + validate_params_func = getattr(cls, "validate_params") + if len(inspect.signature(validate_params_func).parameters) > 1: + raise SyntaxError( + f"Class '{cls.name}' method 'validate_params()' must take exactly 1 " + f"additional arguments (only the implicit 'args')," + f" but it takes {len(inspect.signature(validate_params_func).parameters)}." + ) + + # _compute_qobj method must take only one arguments arg_value + compute_qobj_func = getattr(cls, "_compute_qobj") + if len(inspect.signature(compute_qobj_func).parameters) > 1: + raise SyntaxError( + f"Class '{cls.name}' method '_compute_qobj()' must take exactly 1 " + f"additional arguments (only the implicit 'arg_value')," + f" but it takes {len(inspect.signature(compute_qobj_func).parameters)}." + ) + def __init__(self, *args, arg_label: str | None = None): # This auto triggers a call to arg_value setter (where checks happen) self.arg_value = args @@ -115,7 +135,6 @@ def get_qobj(self) -> Qobj: @staticmethod @abstractmethod def _compute_qobj(args: tuple) -> "Qobj": - """Every child must implement this pure math helper.""" pass def inverse(self, expanded: bool = False) -> Gate: @@ -154,7 +173,5 @@ class AngleParametricGate(ParametricGate): @staticmethod def validate_params(arg_value): for arg in arg_value: - try: - float(arg) - except TypeError: + if not isinstance(arg, Real): raise ValueError(f"Invalid arg {arg} in arg_value") diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 60e069115..62202895f 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -1026,7 +1026,6 @@ def _qasm_defns(self, gate: Gate | Type[Gate]): elif gate == gates.SWAP: gate_def = "gate swap a,b { cx a,b; cx b,a; cx a,b; }" else: - print(gate) err_msg = f"No definition specified for {gate.name} gate" raise NotImplementedError(err_msg) From fb80ef61cfc0a2807eb06fd142dc35d48584a352 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 04:20:35 +0530 Subject: [PATCH 096/117] Added more tests for gates --- tests/test_gates.py | 245 +++++++++++++++++++++++++++++++++---------- tests/test_qiskit.py | 2 - 2 files changed, 191 insertions(+), 56 deletions(-) diff --git a/tests/test_gates.py b/tests/test_gates.py index c18697630..ea694db43 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -273,8 +273,7 @@ def test_two_qubit(self, gate, n_controls): ], ) def test_three_qubit(self, gate: Type[Gate], n_controls): - targets = [qutip.rand_ket(2) for _ in [ -None] * 3] + targets = [qutip.rand_ket(2) for _ in [None] * 3] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(targets) @@ -454,54 +453,94 @@ def test_dtype(self): def test_gate_inverse(gate: Gate | Type[Gate]): n = 2**gate.num_qubits inverse = gate.inverse() - print(gate.name) - print(gate.get_qobj()) - print(inverse.get_qobj()) np.testing.assert_allclose( (gate.get_qobj() * inverse.get_qobj()).full(), np.eye(n), atol=1e-12, ) +def test_gate_equality(): + assert gates.CX == gates.CX + assert gates.CX != gates.CY + assert gates.CX != gates.CRX(0.5) + + assert gates.RX(0.5) == gates.RX(0.5) + assert gates.RX(0.5) != gates.RY(0.5) + assert gates.RX(0.5) != gates.RX(0.6) + + assert gates.CRX(0.5) == gates.CRX(0.5) + assert gates.CRX(0.5) != gates.CRY(0.5) + assert gates.CRX(0.5) != gates.CRX(0.6) + + +class TestGateErrors: + def test_namespace_errors(self): + with pytest.raises(ValueError): + NameSpace("bad.namespace") # namespace cannot contain dots + + ns1 = NameSpace("test_ns") + with pytest.raises(ValueError): + _ = NameSpace("test_ns") # Existing namespace + + class TmpGate(Gate): + num_qubits = 1 + + ns1.register("tmp_gate", TmpGate) + with pytest.raises(NameError, match="already exists in namespace"): + ns1.register("tmp_gate", TmpGate) + + assert ns1.get("tmp") is None + assert ns1.get("tmp_gate") is TmpGate -class TestOperationsErrors: def test_gateclass_errors(self): with pytest.raises(TypeError): + class BadGate(Gate): num_qubits = -1 - def get_qobj(cls): pass + def get_qobj(cls): + pass with pytest.raises(TypeError): + class BadGate2(Gate): num_qubits = 1 is_clifford = 1 # attribute 'is_clifford' must be a bool - def get_qobj(cls): pass + def get_qobj(cls): + pass with pytest.raises(TypeError): + class BadGate3(Gate): num_qubits = 1 self_inverse = 1 # attribute 'self_inverse' must be a bool - def get_qobj(cls): pass + def get_qobj(cls): + pass with pytest.raises(TypeError): + class BadGate4(Gate): num_qubits = 1 self_inverse = True - def get_qobj(cls): pass - def inverse(cls): pass # Can't define inverse method is self_inverse=True + def get_qobj(cls): + pass + + def inverse(cls): + pass # Can't define inverse method is self_inverse=True class GoodGate(Gate): num_qubits = 1 + @staticmethod - def get_qobj(): pass + def get_qobj(): + pass with pytest.raises(AttributeError): + # For a given gateclass, class attribute like num_qubit can't be modified GoodGate.num_qubits = 2 - # For a given gateclass, class attribute like num_qubit can't be modified with pytest.raises(NotImplementedError): GoodGate.inverse() @@ -516,53 +555,109 @@ def get_qobj(): pass U_not_unitary = qutip.Qobj(np.zeros((2, 2))) with pytest.raises(ValueError): - get_unitary_gate("U_not_unitary", U_not_unitary) # U must be unitaru - + get_unitary_gate( + "U_not_unitary", U_not_unitary + ) # U must be unitaru + + with pytest.raises(TypeError): + + class NotGoodGate(GoodGate): + def is_controlled(args): + return args # is_controlled can't take arguments + + with pytest.raises(TypeError): + + class NotGoodGate2(GoodGate): + def is_controlled(): + return 1 # must return a bool + + with pytest.raises(TypeError): + + class NotGoodGate3(GoodGate): + def is_parametric(args): + return args # is_parametric can't take arguments + + with pytest.raises(TypeError): + + class NotGoodGate4(GoodGate): + def is_parametric(): + return 1 # must return a bool + def test_parametric_gate_errors(self): with pytest.raises(TypeError): + class BadParamGate(ParametricGate): num_params = -1 + @staticmethod - def _compute_qobj(args): pass + def _compute_qobj(args): + pass + @staticmethod - def validate_params(args): pass + def validate_params(args): + pass with pytest.raises(TypeError): + class BadParamGate2(ParametricGate): num_params = 1.5 + @staticmethod - def _compute_qobj(args): pass + def _compute_qobj(args): + pass + @staticmethod - def validate_params(args): pass + def validate_params(args): + pass class GoodParamGate(AngleParametricGate): num_qubits = 1 num_params = 2 + @staticmethod - def _compute_qobj(args): return qutip.qeye(2) + def _compute_qobj(args): + return qutip.qeye(2) with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): GoodParamGate(1.0) with pytest.raises(ValueError): - GoodParamGate(1.0, "wrong") # second argument is a string instead of float + GoodParamGate( + 1.0, "wrong" + ) # second argument is a string instead of float gate = GoodParamGate(1.0, 2.0) with pytest.raises(NotImplementedError): gate.inverse() + with pytest.raises(SyntaxError): + + class NotGoodParamGate(GoodParamGate): + def validate_params(arg1, arg2): + pass + + with pytest.raises(SyntaxError): + + class NotGoodParamGate2(GoodParamGate): + def _compute_qobj(arg1, arg2): + pass + def test_controlled_gate_errors(self): from qutip_qip.operations.controlled import ControlledGate import qutip_qip.operations.gates as gates with pytest.raises(TypeError): + class BadCtrlGate(ControlledGate): - target_gate = gates.RX(0) # target_gate must be a gate subclass not an instantiated onject + target_gate = gates.RX( + 0 + ) # target_gate must be a gate subclass not an instantiated onject num_ctrl_qubits = 1 ctrl_value = 1 num_qubits = 2 with pytest.raises(TypeError): + class BadCtrlGate2(ControlledGate): target_gate = gates.X num_ctrl_qubits = -1 # Can't be negative @@ -570,6 +665,7 @@ class BadCtrlGate2(ControlledGate): num_qubits = 2 with pytest.raises(ValueError): + class BadCtrlGate3(ControlledGate): target_gate = gates.X num_qubits = 2 @@ -577,13 +673,17 @@ class BadCtrlGate3(ControlledGate): ctrl_value = 1 with pytest.raises(AttributeError): + class BadCtrlGate4(ControlledGate): target_gate = gates.X - num_ctrl_qubits = 1 # No of control qubits must be 2, since target gate is X + num_ctrl_qubits = ( + 1 # No of control qubits must be 2, since target gate is X + ) ctrl_value = 1 num_qubits = 3 with pytest.raises(TypeError): + class BadCtrlGate5(ControlledGate): target_gate = gates.X num_ctrl_qubits = 1 @@ -591,41 +691,38 @@ class BadCtrlGate5(ControlledGate): num_qubits = 2 with pytest.raises(ValueError): + class BadCtrlGate6(ControlledGate): target_gate = gates.X num_ctrl_qubits = 1 - ctrl_value = 2 # Control value can't be greater than 1 in this case + ctrl_value = ( + 2 # Control value can't be greater than 1 in this case + ) num_qubits = 2 - def test_namespace_errors(self): - with pytest.raises(ValueError): - NameSpace("bad.namespace") # namespace cannot contain dots - - ns1 = NameSpace("test_ns") - with pytest.raises(ValueError): - _ = NameSpace("test_ns") # Existing namespace - - class TmpGate(Gate): - num_qubits = 1 - - ns1.register("tmp_gate", TmpGate) - with pytest.raises(NameError, match="already exists in namespace"): - ns1.register("tmp_gate", TmpGate) - def test_utils_errors(self): from qutip_qip.operations.utils import ( - _check_oper_dims, _targets_to_list, expand_operator, gate_sequence_product + _check_oper_dims, + _targets_to_list, + expand_operator, + gate_sequence_product, ) with pytest.raises(ValueError): - _check_oper_dims(qutip.basis(2, 0)) # The operator is not an Qobj with the same input and output dimensions. + _check_oper_dims( + qutip.basis(2, 0) + ) # The operator is not an Qobj with the same input and output dimensions. op = qutip.qeye(2) with pytest.raises(ValueError): - _check_oper_dims(oper=op, dims=[3], targets=[0]) # The dims don't match + _check_oper_dims( + oper=op, dims=[3], targets=[0] + ) # The dims don't match with pytest.raises(TypeError): - _targets_to_list(1.5, op, 1) # targets should be an integer or a list of integer + _targets_to_list( + 1.5, op, 1 + ) # targets should be an integer or a list of integer with pytest.raises(ValueError): expand_operator(op, 2, 0) # dims needs to be an interable @@ -646,15 +743,55 @@ def test_standard_gates_failures(self): with pytest.raises(TypeError): gates.CRX.get_qobj() -def test_gate_equality(): - assert gates.CX == gates.CX - assert gates.CX != gates.CY - assert gates.CX != gates.CRX(0.5) - - assert gates.RX(0.5) == gates.RX(0.5) - assert gates.RX(0.5) != gates.RY(0.5) - assert gates.RX(0.5) != gates.RX(0.6) + # Control_value > 2^n -1 + with pytest.raises(ValueError): + controlled(gates.X, n_ctrl_qubits=1, control_value=2) - assert gates.CRX(0.5) == gates.CRX(0.5) - assert gates.CRX(0.5) != gates.CRY(0.5) - assert gates.CRX(0.5) != gates.CRX(0.6) + with pytest.raises(TypeError): + controlled(gates.X, n_ctrl_qubits=0) # num_ctrl_qubits > 0 + + def test_class_attribute_modification(self): + with pytest.raises(AttributeError): + gates.CX.namespace = NameSpace("temp") + + with pytest.raises(AttributeError): + gates.X.num_qubits = 0 + + with pytest.raises(AttributeError): + gates.X.is_clifford = False + + with pytest.raises(AttributeError): + gates.X.self_inverse = False + + with pytest.raises(AttributeError): + gates.CX.num_ctrl_qubits = 0 + + with pytest.raises(AttributeError): + gates.CX.ctrl_value = 0 + + with pytest.raises(AttributeError): + gates.CX.target_gate = gates.Y + + with pytest.raises(AttributeError): + gates.RX.num_params = 2 + + with pytest.raises(AttributeError): + gates.RY.latex_str = "R_y" + + class H(Gate): + num_qubits = 1 + + def get_qobj(): + pass + + assert H.name == "H" + assert H.latex_str == "H" + + class H(Gate): + name = "Hadamard" + num_qubits = 1 + + def get_qobj(): + pass + + H.name = "Hadamard" diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index 17a575aa1..2287d2786 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -102,8 +102,6 @@ def _compare_circuit( for i, res_ins in enumerate(result_circuit.instructions): req_ins = required_circuit.instructions[i] - print(req_ins) - print(res_ins) if not self._compare_gate_instructions( req_ins, res_ins, result_circuit From 15f8250be658ac49a05ddd6c93549cbe5e8dfd4f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 04:43:14 +0530 Subject: [PATCH 097/117] Add argument dtype to get_qobj() method Although most gates are dense, dtype will significantly help us in setting up get_qobj for JAX, CUDA backends. --- src/qutip_qip/operations/controlled.py | 6 +- src/qutip_qip/operations/gateclass.py | 8 +- src/qutip_qip/operations/gates/other_gates.py | 13 ++- .../operations/gates/single_qubit_gate.py | 103 +++++++++--------- .../operations/gates/two_qubit_gate.py | 91 ++++++++++------ src/qutip_qip/operations/parametric.py | 20 ++-- src/qutip_qip/operations/utils.py | 2 +- tests/test_compiler.py | 4 +- tests/test_gates.py | 9 +- 9 files changed, 143 insertions(+), 113 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 4c336b73f..cb9b4b16d 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -166,7 +166,7 @@ def _validate_control_value(cls) -> None: ) @class_or_instance_method - def get_qobj(cls_or_self) -> Qobj: + def get_qobj(cls_or_self, dtype: str = "dense") -> Qobj: """ Construct the full Qobj representation of the controlled gate. @@ -177,13 +177,13 @@ def get_qobj(cls_or_self) -> Qobj: """ if isinstance(cls_or_self, type): return controlled_gate_unitary( - U=cls_or_self.target_gate.get_qobj(), + U=cls_or_self.target_gate.get_qobj(dtype), num_controls=cls_or_self.num_ctrl_qubits, control_value=cls_or_self.ctrl_value, ) return controlled_gate_unitary( - U=cls_or_self._target_inst.get_qobj(), + U=cls_or_self._target_inst.get_qobj(dtype), num_controls=cls_or_self.num_ctrl_qubits, control_value=cls_or_self.ctrl_value, ) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 8807d2caf..a0aa61f91 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -258,7 +258,7 @@ def __init__(self) -> None: @staticmethod @abstractmethod - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: """ Get the :class:`qutip.Qobj` representation of the gate operator. @@ -267,7 +267,7 @@ def get_qobj() -> Qobj: qobj : :obj:`qutip.Qobj` The compact gate operator as a unitary matrix. """ - pass + raise NotImplementedError @classmethod def inverse(cls) -> Type[Gate]: @@ -336,7 +336,7 @@ class _CustomGate(Gate): self_inverse = U == U.dag() @staticmethod - def get_qobj(): - return U + def get_qobj(dtype=U.dtype): + return U.to(dtype) return _CustomGate diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index d8428cf2d..7185ffe87 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -33,18 +33,19 @@ def __init__(self, phase: float = 0.0): def __repr__(self): return f"Gate({self.name}, phase {self.arg_value[0]})" - def _compute_qobj(): + def compute_qobj(): raise NotImplementedError - def get_qobj(self, num_qubits=None): + def get_qobj(self, num_qubits=None, dtype: str = "dense"): phase = self.arg_value[0] if num_qubits is None: - return Qobj(phase) + return Qobj(phase, dtype=dtype) N = 2**num_qubits return Qobj( np.exp(1.0j * phase) * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], + dtype=dtype, ) @@ -81,7 +82,7 @@ class TOFFOLI(ControlledGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -94,6 +95,7 @@ def get_qobj() -> Qobj: [0, 0, 0, 0, 0, 0, 1, 0], ], dims=[[2, 2, 2], [2, 2, 2]], + dtype=dtype, ) @@ -130,7 +132,7 @@ class FREDKIN(ControlledGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -143,4 +145,5 @@ def get_qobj() -> Qobj: [0, 0, 0, 0, 0, 0, 0, 1], ], dims=[[2, 2, 2], [2, 2, 2]], + dtype=dtype, ) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index d27a1e630..5337ae537 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -46,8 +46,8 @@ class X(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return sigmax(dtype="dense") + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmax(dtype=dtype) class Y(_SingleQubitGate): @@ -72,8 +72,8 @@ class Y(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return sigmay(dtype="dense") + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmay(dtype=dtype) class Z(_SingleQubitGate): @@ -98,8 +98,8 @@ class Z(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return sigmaz(dtype="dense") + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmaz(dtype=dtype) class IDLE(_SingleQubitGate): @@ -119,8 +119,8 @@ class IDLE(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return qeye(2) + def get_qobj(dtype: str = "dense") -> Qobj: + return qeye(2, dtype=dtype) class H(_SingleQubitGate): @@ -145,25 +145,12 @@ class H(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return 1 / np.sqrt(2.0) * Qobj([[1, 1], [1, -1]]) + def get_qobj(dtype: str = "dense") -> Qobj: + sq_half = 1 / np.sqrt(2.0) + return Qobj([[sq_half, sq_half], [sq_half, -sq_half]], dtype=dtype) -class SNOT(H): - """ - Hadamard gate (Deprecated, use H instead). - """ - - __slots__ = () - - def __init__(self): - warnings.warn( - "SNOT is deprecated and will be removed in future versions. " - "Use H instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() +SNOT = H class SQRTX(_SingleQubitGate): @@ -188,8 +175,10 @@ class SQRTX(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]], dtype=dtype + ) @staticmethod def inverse() -> Type[Gate]: @@ -218,8 +207,10 @@ class SQRTXdag(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]], dtype=dtype + ) @staticmethod def inverse() -> Type[Gate]: @@ -261,8 +252,8 @@ class S(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[1, 0], [0, 1j]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, 1j]], dtype=dtype) @staticmethod def inverse() -> Type[Gate]: @@ -291,8 +282,8 @@ class Sdag(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[1, 0], [0, -1j]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, -1j]], dtype=dtype) @staticmethod def inverse() -> Type[Gate]: @@ -320,8 +311,8 @@ class T(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]], dtype=dtype) @staticmethod def inverse() -> Type[Gate]: @@ -349,8 +340,8 @@ class Tdag(_SingleQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: - return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]]) + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]], dtype=dtype) @staticmethod def inverse() -> Type[Gate]: @@ -381,7 +372,7 @@ def __init__(self, theta: float, arg_label=None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: phi = arg_value[0] return Qobj( [ @@ -389,6 +380,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [-1j * np.sin(phi / 2), np.cos(phi / 2)], ], dims=[[2], [2]], + dtype=dtype, ) def inverse( @@ -425,13 +417,15 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: phi = arg_value[0] return Qobj( [ [np.cos(phi / 2), -np.sin(phi / 2)], [np.sin(phi / 2), np.cos(phi / 2)], - ] + ], + dims=[[2], [2]], + dtype=dtype, ) def inverse( @@ -468,9 +462,13 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: phi = arg_value[0] - return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) + return Qobj( + [[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]], + dims=[[2], [2]], + dtype=dtype, + ) def inverse( self, expanded: bool = False @@ -501,13 +499,12 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: phi = arg_value[0] return Qobj( - [ - [1, 0], - [0, np.exp(1j * phi)], - ] + [[1, 0], [0, np.exp(1j * phi)]], + dims=[[2], [2]], + dtype=dtype, ) def inverse( @@ -551,7 +548,7 @@ def __init__(self, phi: float, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: + def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: phi, theta = arg_value return Qobj( [ @@ -563,7 +560,9 @@ def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: -1.0j * np.exp(1.0j * phi) * np.sin(theta / 2.0), np.cos(theta / 2.0), ], - ] + ], + dims=[[2], [2]], + dtype=dtype, ) def inverse( @@ -611,7 +610,7 @@ def __init__( @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta, phi, gamma = arg_value return Qobj( [ @@ -623,7 +622,9 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2), ], - ] + ], + dims=[[2], [2]], + dtype=dtype, ) def inverse( diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 131412cd7..71505fce6 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -72,10 +72,11 @@ class SWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -103,10 +104,11 @@ class ISWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -138,10 +140,11 @@ class ISWAPdag(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 0, -1j, 0], [0, -1j, 0, 0], [0, 0, 0, 1]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -172,7 +175,7 @@ class SQRTSWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( np.array( [ @@ -183,6 +186,7 @@ def get_qobj() -> Qobj: ] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -213,7 +217,7 @@ class SQRTSWAPdag(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( np.array( [ @@ -224,6 +228,7 @@ def get_qobj() -> Qobj: ] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -255,7 +260,7 @@ class SQRTISWAP(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( np.array( [ @@ -266,6 +271,7 @@ def get_qobj() -> Qobj: ] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -297,7 +303,7 @@ class SQRTISWAPdag(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( np.array( [ @@ -308,6 +314,7 @@ def get_qobj() -> Qobj: ] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -347,7 +354,7 @@ class BERKELEY(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [np.cos(np.pi / 8), 0, 0, 1.0j * np.sin(np.pi / 8)], @@ -356,6 +363,7 @@ def get_qobj() -> Qobj: [1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -395,7 +403,7 @@ class BERKELEYdag(_TwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [np.cos(np.pi / 8), 0, 0, -1.0j * np.sin(np.pi / 8)], @@ -404,6 +412,7 @@ def get_qobj() -> Qobj: [-1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @staticmethod @@ -446,7 +455,7 @@ def __init__(self, alpha: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: alpha = arg_value[0] return Qobj( [ @@ -466,6 +475,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [0, 0, 0, 1], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) def inverse( @@ -513,7 +523,7 @@ def __init__(self, theta: float, phi: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: + def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: theta, phi = arg_value return Qobj( [ @@ -533,6 +543,7 @@ def _compute_qobj(arg_value: tuple[float, float]) -> Qobj: ], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) def inverse( @@ -582,7 +593,7 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta = arg_value[0] return Qobj( np.array( @@ -594,6 +605,7 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: ] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) def inverse( @@ -630,10 +642,11 @@ class CX(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -664,10 +677,11 @@ class CY(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -695,10 +709,11 @@ class CZ(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -730,7 +745,7 @@ class CH(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: sq_2 = 1 / np.sqrt(2) return Qobj( [ @@ -740,6 +755,7 @@ def get_qobj() -> Qobj: [0, 0, sq_2, -sq_2], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -768,7 +784,7 @@ class CT(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [1, 0, 0, 0], @@ -777,6 +793,7 @@ def get_qobj() -> Qobj: [0, 0, 0, (1 + 1j) / np.sqrt(2)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -805,12 +822,13 @@ class CS(_ControlledTwoQubitGate): @staticmethod @cache - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( np.array( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] ), dims=[[2, 2], [2, 2]], + dtype=dtype, ) @@ -834,7 +852,7 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta = arg_value[0] return Qobj( [ @@ -844,10 +862,11 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [0, 0, -1j * np.sin(theta / 2), np.cos(theta / 2)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype=dtype) class CRY(_ControlledTwoQubitGate): @@ -870,7 +889,7 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta = arg_value[0] return Qobj( [ @@ -880,10 +899,11 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [0, 0, np.sin(theta / 2), np.cos(theta / 2)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) class CRZ(_ControlledTwoQubitGate): @@ -922,7 +942,7 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta = arg_value[0] return Qobj( [ @@ -932,10 +952,11 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [0, 0, 0, np.exp(1j * theta / 2)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) class CPHASE(_ControlledTwoQubitGate): @@ -974,7 +995,7 @@ def __init__(self, theta: float, arg_label: str | None = None): @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float]) -> Qobj: + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: theta = arg_value[0] return Qobj( [ @@ -984,10 +1005,11 @@ def _compute_qobj(arg_value: tuple[float]) -> Qobj: [0, 0, 0, np.exp(1j * theta)], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) class CQASMU(_ControlledTwoQubitGate): @@ -1016,7 +1038,9 @@ def __init__( @staticmethod @lru_cache(maxsize=128) - def _compute_qobj(arg_value: tuple[float, float, float]) -> Qobj: + def compute_qobj( + arg_value: tuple[float, float, float], dtype: str + ) -> Qobj: theta, phi, gamma = arg_value return Qobj( [ @@ -1036,7 +1060,8 @@ def _compute_qobj(arg_value: tuple[float, float, float]) -> Qobj: ], ], dims=[[2, 2], [2, 2]], + dtype=dtype, ) - def get_qobj(self) -> Qobj: - return self._compute_qobj(self.arg_value) + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype=dtype) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 2947b8131..f775d04b3 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -74,12 +74,12 @@ def __init_subclass__(cls, **kwargs) -> None: f" but it takes {len(inspect.signature(validate_params_func).parameters)}." ) - # _compute_qobj method must take only one arguments arg_value - compute_qobj_func = getattr(cls, "_compute_qobj") - if len(inspect.signature(compute_qobj_func).parameters) > 1: + # compute_qobj method must take only two arguments arg_value, dtype + compute_qobj_func = getattr(cls, "compute_qobj") + if len(inspect.signature(compute_qobj_func).parameters) > 2: raise SyntaxError( - f"Class '{cls.name}' method '_compute_qobj()' must take exactly 1 " - f"additional arguments (only the implicit 'arg_value')," + f"Class '{cls.name}' method 'compute_qobj()' must take exactly 1 " + f"additional arguments (only the implicit 'arg_value, dtype')," f" but it takes {len(inspect.signature(compute_qobj_func).parameters)}." ) @@ -119,9 +119,9 @@ def validate_params(arg_value): arg_value : list of float The parameters to validate. """ - pass + raise NotImplementedError - def get_qobj(self) -> Qobj: + def get_qobj(self, dtype: str = "dense") -> Qobj: """ Get the QuTiP quantum object representation using the current parameters. @@ -130,12 +130,12 @@ def get_qobj(self) -> Qobj: qobj : qutip.Qobj The unitary matrix representing the gate with the specific `arg_value`. """ - return self._compute_qobj(self.arg_value) + return self.compute_qobj(self.arg_value, dtype) @staticmethod @abstractmethod - def _compute_qobj(args: tuple) -> "Qobj": - pass + def compute_qobj(args: tuple, dtype: str) -> Qobj: + raise NotImplementedError def inverse(self, expanded: bool = False) -> Gate: if self.self_inverse: diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index 47034a95e..d6059fbfa 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -153,7 +153,7 @@ def expand_operator( The indices of subspace that are acted on. Permutation can also be realized by changing the orders of the indices. dtype : str, optional - Data type of the output `Qobj`. Only for qutip version larger than 5. + Data type of the output `Qobj`. Returns diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 653f1f366..484af485f 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -85,7 +85,7 @@ class U1(AngleParametricGate): num_params = 1 self_inverse = False - def _compute_qobj(self): + def compute_qobj(self): pass class U2(AngleParametricGate): @@ -93,7 +93,7 @@ class U2(AngleParametricGate): num_params = 1 self_inverse = False - def _compute_qobj(): + def compute_qobj(): pass num_qubits = 2 diff --git a/tests/test_gates.py b/tests/test_gates.py index ea694db43..c2dbf18f4 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -459,6 +459,7 @@ def test_gate_inverse(gate: Gate | Type[Gate]): atol=1e-12, ) + def test_gate_equality(): assert gates.CX == gates.CX assert gates.CX != gates.CY @@ -590,7 +591,7 @@ class BadParamGate(ParametricGate): num_params = -1 @staticmethod - def _compute_qobj(args): + def compute_qobj(args): pass @staticmethod @@ -603,7 +604,7 @@ class BadParamGate2(ParametricGate): num_params = 1.5 @staticmethod - def _compute_qobj(args): + def compute_qobj(args): pass @staticmethod @@ -615,7 +616,7 @@ class GoodParamGate(AngleParametricGate): num_params = 2 @staticmethod - def _compute_qobj(args): + def compute_qobj(args): return qutip.qeye(2) with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): @@ -639,7 +640,7 @@ def validate_params(arg1, arg2): with pytest.raises(SyntaxError): class NotGoodParamGate2(GoodParamGate): - def _compute_qobj(arg1, arg2): + def compute_qobj(arg1, arg2, dtype): pass def test_controlled_gate_errors(self): From fd848b88f650c640eecc95eaf1fcdde7967bd797 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 05:26:39 +0530 Subject: [PATCH 098/117] Added the functionality to remove elements from a namespace --- src/qutip_qip/operations/namespace.py | 7 +++++++ tests/test_gates.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index 9bcd56abb..128635194 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -77,6 +77,13 @@ def get(self, name: str | tuple[str, int, int]) -> any: return None return self._registry[name] + def _remove(self, name: str | tuple[str, int, int]) -> None: + if name not in self._registry: + raise KeyError( + f"{name} does not exists in namespace '{self.name} " + ) + del self._registry[name] + def __hash__(self) -> int: return hash(self.name) diff --git a/tests/test_gates.py b/tests/test_gates.py index c2dbf18f4..cca1c6875 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -493,6 +493,10 @@ class TmpGate(Gate): assert ns1.get("tmp") is None assert ns1.get("tmp_gate") is TmpGate + ns1._remove("tmp_gate") + with pytest.raises(KeyError): + ns1._remove("tmp_gate") + def test_gateclass_errors(self): with pytest.raises(TypeError): From 100234d3be7af034c71079654676601f71e34be5 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 05:30:53 +0530 Subject: [PATCH 099/117] Add checks on is_controlled, is_parametric method --- src/qutip_qip/operations/controlled.py | 5 +++++ src/qutip_qip/operations/parametric.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index cb9b4b16d..ce4ed7e12 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -110,6 +110,11 @@ def __init_subclass__(cls, **kwargs) -> None: 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 always return True." + ) + def __init__(self, *args, **kwargs) -> None: self._target_inst = self.target_gate(*args, **kwargs) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index f775d04b3..82c8d6378 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -83,6 +83,11 @@ def __init_subclass__(cls, **kwargs) -> None: f" but it takes {len(inspect.signature(compute_qobj_func).parameters)}." ) + if not cls.is_parametric(): + raise ValueError( + f"Class '{cls.name}' method 'is_parametric()' must always return True." + ) + def __init__(self, *args, arg_label: str | None = None): # This auto triggers a call to arg_value setter (where checks happen) self.arg_value = args From 403d6560bec33b3190d623084b1aeea3d870e07c Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 05:36:39 +0530 Subject: [PATCH 100/117] Minor code ordering change in add_gate method --- src/qutip_qip/circuit/circuit.py | 66 ++++++++++++++++---------- src/qutip_qip/operations/parametric.py | 2 +- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 83fa7e744..6741c0d71 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -3,17 +3,21 @@ """ import warnings +import inspect from typing import Iterable, Type from qutip import qeye, Qobj import numpy as np -from ._decompose import _resolve_to_universal, _resolve_2q_basis from qutip_qip.circuit import ( CircuitSimulator, CircuitInstruction, GateInstruction, MeasurementInstruction, ) +from qutip_qip.circuit._decompose import ( + _resolve_to_universal, + _resolve_2q_basis, +) from qutip_qip.operations import Gate, Measurement, expand_operator from qutip_qip.operations import gates as std from qutip_qip.typing import Int, IntSequence @@ -305,37 +309,31 @@ def add_gate( self.add_global_phase(gate.arg_value[0]) return - # Handling case for integer input - targets = convert_type_input_to_sequence(int, "targets", targets) - controls = convert_type_input_to_sequence(int, "controls", controls) - classical_controls = convert_type_input_to_sequence( - int, "classical_controls", classical_controls - ) - - # Checks each element within the limit - check_limit("targets", targets, 0, self.num_qubits - 1) - check_limit("controls", controls, 0, self.num_qubits - 1) - check_limit( - "classical_controls", classical_controls, 0, self.num_cbits - 1 - ) - # This conditional block can be remove if the gate input is only # restricted to Gate subclasses or object instead of strings in the future. if not isinstance(gate, Gate): - if type(gate) is str and gate in std.GATE_CLASS_MAP: - warnings.warn( - "Passing Gate as a string input has been deprecated and will be removed in future versions.", - DeprecationWarning, - stacklevel=2, - ) - gate_class = std.GATE_CLASS_MAP[gate] + if type(gate) is str: + if gate in std.GATE_CLASS_MAP: + warnings.warn( + "Passing Gate as a string input has been deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=2, + ) + gate_class = std.GATE_CLASS_MAP[gate] + + else: + raise KeyError( + "Can only pass standard gate name as strings" + "or Gate class or its object instantiation" + ) elif issubclass(gate, Gate): gate_class = gate + else: - raise ValueError( - "Can only pass standard gate name as strings" - "or Gate class or its object instantiation" + raise TypeError( + "gate must be of Gate type or oject or a string ", + f"got {type(gate)} instead." ) if gate_class.is_parametric(): @@ -345,7 +343,9 @@ def add_gate( gate = gate_class # Check for gates - if not (isinstance(gate, Gate) or issubclass(gate, Gate)): + if inspect.isabstract(gate): + raise TypeError("gate must not be an abstract class") + elif not (isinstance(gate, Gate) or issubclass(gate, Gate)): raise TypeError(f"gate must be of type Gate, got {gate}") elif gate.is_parametric() and (not isinstance(gate, Gate)): raise TypeError( @@ -356,6 +356,20 @@ def add_gate( "You must pass a Gate type for a non-parametrized gate" ) + # Handling case for integer input + targets = convert_type_input_to_sequence(int, "targets", targets) + controls = convert_type_input_to_sequence(int, "controls", controls) + classical_controls = convert_type_input_to_sequence( + int, "classical_controls", classical_controls + ) + + # Checks each element within the limit + check_limit("targets", targets, 0, self.num_qubits - 1) + check_limit("controls", controls, 0, self.num_qubits - 1) + check_limit( + "classical_controls", classical_controls, 0, self.num_cbits - 1 + ) + # Check len(controls) == gate.num_ctrl_qubits if gate.is_controlled() and len(controls) != gate.num_ctrl_qubits: raise ValueError( diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 82c8d6378..87e1408af 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -179,4 +179,4 @@ class AngleParametricGate(ParametricGate): def validate_params(arg_value): for arg in arg_value: if not isinstance(arg, Real): - raise ValueError(f"Invalid arg {arg} in arg_value") + raise TypeError(f"Invalid arg {arg} in arg_value") From c5ca576eeda387caa5e2b880b236ecf10d5f4551 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Mon, 9 Mar 2026 06:13:55 +0530 Subject: [PATCH 101/117] Add test for add_gate method in test_circuit.py Most testing for input cases which should raise error both in add_gate and in case of CircuitInstructions --- src/qutip_qip/circuit/circuit.py | 2 +- tests/test_circuit.py | 155 ++++++++++++++++++++++++++++++- tests/test_gates.py | 2 +- 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 6741c0d71..0e7e371b6 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -333,7 +333,7 @@ def add_gate( else: raise TypeError( "gate must be of Gate type or oject or a string ", - f"got {type(gate)} instead." + f"got {type(gate)} instead.", ) if gate_class.is_parametric(): diff --git a/tests/test_circuit.py b/tests/test_circuit.py index b07b67845..3c0dc7da8 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -18,7 +18,13 @@ identity, ) -from qutip_qip.circuit import QubitCircuit, CircuitSimulator +from qutip_qip.circuit import ( + QubitCircuit, + CircuitSimulator, + CircuitInstruction, + GateInstruction, + MeasurementInstruction, +) from qutip_qip.circuit.draw import TeXRenderer from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation from qutip_qip.operations import Gate, Measurement, gate_sequence_product @@ -771,6 +777,153 @@ def test_circuit_chain_structure(self): assert qc2.input_states == [None] * 3 +class TestAddGateError: + def test_add_gate_errors(self): + qc = QubitCircuit(3, num_cbits=1) + + with pytest.raises(KeyError): + qc.add_gate("123") # Can only pass standard gate name as strings + + with pytest.raises(TypeError): + + class BadGate: ... # Doesn't inherit for Gate + + qc.add_gate(BadGate) + + with pytest.raises(TypeError): + + class AbstractGate( + Gate + ): ... # Doesn't define num_qubits, get_qobj + + qc.add_gate(AbstractGate) + + with pytest.raises(TypeError): + qc.add_gate(gates.RX, targets=[0]) # RX must be initialized + + with pytest.raises(TypeError): + qc.add_gate(gates.X(), targets=[0]) # X can't be initialized + + with pytest.raises(ValueError): + qc.add_gate( + gates.CX, targets=[], controls=[] + ) # targets, controls are empty + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[0], controls=[]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[1, 2], controls=[]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[1], controls=[-1]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[-1], controls=[1]) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[-1], # Each entry must be non -negative + classical_control_value=1, + ) + + with pytest.raises(TypeError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value="1", # Can't be string + ) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value=2, # Incorrect + ) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value=-1, # Can't be negative + ) + + +class TestInstructionErrors: + def test_instruction_post_init_errors(self): + class MockInstruction(CircuitInstruction): + def to_qasm(self, qasm_out): + pass + + def __str__(self): + return "" + + with pytest.raises(ValueError): + # Circuit Instruction must operate on at least one qubit or cbit + MockInstruction("gate", qubits=(), cbits=()) + + with pytest.raises(TypeError): + MockInstruction( + "gate", qubits=[0], cbits=() + ) # Must pass a tuple for qubits + + with pytest.raises(ValueError): + MockInstruction( + "gate", qubits=(0.5,), cbits=() + ) # All qubit indices must be an int + + with pytest.raises(ValueError): + MockInstruction( + "gate", qubits=(-1,), cbits=() + ) # qubit indices must be non-negative + + with pytest.raises(ValueError, match="Found repeated qubits"): + MockInstruction("gate", qubits=(0, 0), cbits=()) + + with pytest.raises(ValueError, match="Found repeated cbits"): + MockInstruction("gate", qubits=(0,), cbits=(0, 0)) + + def test_gate_instruction_errors(self): + with pytest.raises(TypeError): + GateInstruction(operation="not a gate", qubits=(0,)) + + with pytest.raises(ValueError): + GateInstruction( + operation=gates.X, qubits=(0, 1) + ) # requires 1 qubits + + with pytest.raises(ValueError): + GateInstruction(operation=gates.X, qubits=(0,), cbits=(0,)) + # cbits_ctrl_value can't be None if classical controls are provided + + with pytest.raises(ValueError): + # Classical Control value can't be negative + GateInstruction( + operation=gates.X, qubits=(0,), cbits=(0,), cbits_ctrl_value=-1 + ) + + with pytest.raises(ValueError): + # Classical Control value can't be greater than 1 in this case + GateInstruction( + operation=gates.X, qubits=(0,), cbits=(0,), cbits_ctrl_value=2 + ) + + def test_measurement_instruction_errors(self): + with pytest.raises(TypeError): + # Operation must be of type Measurement + MeasurementInstruction(operation="M0", qubits=(0,), cbits=(0,)) + + meas = Measurement("M0", targets=[0], classical_store=0) + with pytest.raises(ValueError): + # Measurement requires equal number of qubits and cbits + MeasurementInstruction(operation=meas, qubits=(0, 1), cbits=(0,)) + + def test_gates_class(): init_state = qutip.rand_ket([2, 2, 2]) diff --git a/tests/test_gates.py b/tests/test_gates.py index cca1c6875..5960924df 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -626,7 +626,7 @@ def compute_qobj(args): with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): GoodParamGate(1.0) - with pytest.raises(ValueError): + with pytest.raises(TypeError): GoodParamGate( 1.0, "wrong" ) # second argument is a string instead of float From 93dcaa04c9a3166c8ad9257c0082c601bbafd679 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 21:35:35 +0530 Subject: [PATCH 102/117] Rename controlled to get_controlled_gate --- src/qutip_qip/algorithms/qpe.py | 4 ++-- src/qutip_qip/circuit/circuit.py | 2 +- src/qutip_qip/operations/__init__.py | 4 ++-- src/qutip_qip/operations/controlled.py | 6 +++--- src/qutip_qip/operations/gateclass.py | 23 +---------------------- tests/test_gates.py | 12 +++++------- tests/test_qpe.py | 4 ++-- tests/test_renderer.py | 8 ++++---- 8 files changed, 20 insertions(+), 43 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 1a702229e..30c6764bc 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,7 @@ import numpy as np from qutip_qip.algorithms import qft_gate_sequence from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import get_unitary_gate, controlled +from qutip_qip.operations import get_unitary_gate, get_controlled_gate from qutip_qip.operations.gates import H @@ -63,7 +63,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): U_power = U if power == 1 else U**power # Add controlled-U^power gate - controlled_u = controlled( + controlled_u = get_controlled_gate( gate=get_unitary_gate( gate_name=f"U^{power}", U=U_power, diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 0e7e371b6..0a5edf54d 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -300,7 +300,7 @@ def add_gate( "'control_value' is no longer a valid argument and has been deprecated and will be removed in the future version. " "from qutip_qip.operations import controlled", "from qutip_qip.operations.gates import X", - "Example: gate = controlled(X, num_ctrl_qubits=1, control_value=0) instead", + "Example: gate = get_controlled_gate(X, num_ctrl_qubits=1, control_value=0) instead", DeprecationWarning, stacklevel=2, ) diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index 5b46c7bb2..3a15ffa82 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -11,7 +11,7 @@ ) from .gateclass import Gate, get_unitary_gate from .parametric import ParametricGate, AngleParametricGate -from .controlled import ControlledGate, controlled +from .controlled import ControlledGate, get_controlled_gate from .measurement import Measurement from .old_gates import ( rx, @@ -54,7 +54,7 @@ "ParametricGate", "ControlledGate", "get_unitary_gate", - "controlled", + "get_controlled_gate", "AngleParametricGate", "Measurement", "rx", diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index ce4ed7e12..3b21c5bd3 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -200,7 +200,7 @@ def inverse(cls_or_self) -> Gate | Type[Gate]: # Non-parametrized Gates e.g. S elif isinstance(cls_or_self, type): - inverse_gate = controlled( + inverse_gate = get_controlled_gate( cls_or_self.target_gate.inverse(), cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value, @@ -211,7 +211,7 @@ def inverse(cls_or_self) -> Gate | Type[Gate]: expanded=True ) - inverse_gate = controlled( + inverse_gate = get_controlled_gate( inverse_gate_class, cls_or_self.num_ctrl_qubits, cls_or_self.ctrl_value, @@ -246,7 +246,7 @@ def __hash__(self) -> int: return super().__hash__() -def controlled( +def get_controlled_gate( gate: Type[Gate], n_ctrl_qubits: int = 1, control_value: int | None = None, diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index a0aa61f91..5e5b3ed17 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -52,7 +52,7 @@ def __init__(cls, name, bases, attrs): cls.namespace.register(cls.name, cls) # For lookup dictionary for Controlled Gates - # e.g. controlled(X, num_ctrl_qubits=1, ctrl_value=1) this is CX, + # e.g. get_controlled_gate(X, num_ctrl_qubits=1, ctrl_value=1) this is CX, # why do we have to define it again if it already exists. if cls.is_controlled() and getattr(cls, "namespace", None) is not None: cls.namespace.register( @@ -87,27 +87,6 @@ def __str__(cls) -> str: def __repr__(cls) -> str: return f"Gate({cls.name}, num_qubits={cls.num_qubits})" - def __eq__(cls, other: any) -> bool: - # Return False if other is not a class. - if not isinstance(other, type): - return False - - # 'is' keyword in Python checks check if two variables refer to - # the exact same object in memory, since non-parametrized gate classes - # are not meant to be parametrized it works, ParametrizedGate class - # has its own __eq__ method which acts on an instance. - if cls is other: - return True - return False - - def __hash__(cls) -> int: - """ - Required because __eq__ is overridden. - Hashes the class based on its unique identity (namespace and name) - so it can still be safely used in the _registry sets and dicts. - """ - return id(cls) # By default Python using id() for hashing - class Gate(ABC, metaclass=_GateMetaClass): r""" diff --git a/tests/test_gates.py b/tests/test_gates.py index 5960924df..749bac6aa 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -8,10 +8,11 @@ from qutip_qip.operations import ( AngleParametricGate, + ControlledGate, Gate, ParametricGate, NameSpace, - controlled, + get_controlled_gate, expand_operator, get_unitary_gate, hadamard_transform, @@ -262,7 +263,7 @@ def test_two_qubit(self, gate, n_controls): assert _infidelity(test, expected) < 1e-12 random_gate = get_unitary_gate("random", qutip.rand_unitary([2] * 1)) - RandomThreeQubitGate = controlled(random_gate, 2) + RandomThreeQubitGate = get_controlled_gate(random_gate, 2) @pytest.mark.parametrize( ["gate", "n_controls"], @@ -648,9 +649,6 @@ def compute_qobj(arg1, arg2, dtype): pass def test_controlled_gate_errors(self): - from qutip_qip.operations.controlled import ControlledGate - import qutip_qip.operations.gates as gates - with pytest.raises(TypeError): class BadCtrlGate(ControlledGate): @@ -750,10 +748,10 @@ def test_standard_gates_failures(self): # Control_value > 2^n -1 with pytest.raises(ValueError): - controlled(gates.X, n_ctrl_qubits=1, control_value=2) + get_controlled_gate(gates.X, n_ctrl_qubits=1, control_value=2) with pytest.raises(TypeError): - controlled(gates.X, n_ctrl_qubits=0) # num_ctrl_qubits > 0 + get_controlled_gate(gates.X, n_ctrl_qubits=0) # num_ctrl_qubits > 0 def test_class_attribute_modification(self): with pytest.raises(AttributeError): diff --git a/tests/test_qpe.py b/tests/test_qpe.py index f7f9d93b8..6ae1a5cbf 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -4,7 +4,7 @@ from qutip import Qobj, sigmaz, tensor from qutip_qip.algorithms.qpe import qpe -from qutip_qip.operations import controlled, get_unitary_gate +from qutip_qip.operations import get_controlled_gate, get_unitary_gate from qutip_qip.operations import gates as std @@ -29,7 +29,7 @@ def test_controlled_unitary(self): """ U = Qobj([[0, 1], [1, 0]]) - controlled_u = controlled( + controlled_u = get_controlled_gate( gate=get_unitary_gate(gate_name="CU", U=U), ) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 9639c0190..cd27f492c 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -3,7 +3,7 @@ from unittest.mock import patch from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer -from qutip_qip.operations import controlled +from qutip_qip.operations import get_controlled_gate from qutip_qip.operations.gates import ( IDLE, X, @@ -164,9 +164,9 @@ def qc3(): @pytest.fixture def qc4(): - i = controlled(IDLE, n_ctrl_qubits=1, gate_name="i") - ii = controlled(IDLE, n_ctrl_qubits=2, gate_name="ii") - iii = controlled(IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii") + i = get_controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="i") + ii = get_controlled_gate(IDLE, n_ctrl_qubits=2, gate_name="ii") + iii = get_controlled_gate(IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii") qc = QubitCircuit(5, num_cbits=2) qc.add_gate( From 45ebe8149c571ef68aee2c2c60574aecb649cb7e Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 21:41:50 +0530 Subject: [PATCH 103/117] Fix __hash__ --- src/qutip_qip/operations/controlled.py | 4 +++- src/qutip_qip/operations/namespace.py | 11 ++++++++--- src/qutip_qip/operations/parametric.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 3b21c5bd3..a0fc7bafd 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -243,7 +243,9 @@ def __eq__(self, other) -> bool: return True def __hash__(self) -> int: - return super().__hash__() + if self.is_parametric(): + return hash((type(self), self._target_inst)) + return hash(type(self)) def get_controlled_gate( diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index 128635194..50a62baf6 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -84,15 +84,20 @@ def _remove(self, name: str | tuple[str, int, int]) -> None: ) del self._registry[name] - def __hash__(self) -> int: - return hash(self.name) - def __str__(self) -> str: return self.name def __repr__(self): return str(self) + def __eq__(self, other) -> bool: + if type(other) is not NameSpace: + return False + return self.name == other.name + + def __hash__(self) -> int: + return hash(self.name) + NS_STD = NameSpace("std") # DEFAULT NAMESPACE NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 87e1408af..d4554cd3b 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -169,7 +169,7 @@ def __eq__(self, other) -> bool: return True def __hash__(self) -> int: - return super().__hash__() + return hash((type(self), self.arg_value)) class AngleParametricGate(ParametricGate): From 6b10f95a69265a3f0d5023c88e387577c84ca138 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 21:46:06 +0530 Subject: [PATCH 104/117] Removed redundant CNOT in the codebase --- src/qutip_qip/circuit/_decompose.py | 8 ++++---- src/qutip_qip/circuit/circuit.py | 12 ++---------- src/qutip_qip/circuit/draw/color_theme.py | 4 ---- src/qutip_qip/circuit/draw/mat_renderer.py | 2 +- src/qutip_qip/circuit/draw/texrenderer.py | 2 +- src/qutip_qip/compiler/circuitqedcompiler.py | 1 - src/qutip_qip/compiler/scheduler.py | 4 ++-- src/qutip_qip/device/circuitqed.py | 2 +- src/qutip_qip/operations/gates/__init__.py | 2 +- src/qutip_qip/qasm.py | 1 - src/qutip_qip/transpiler/chain.py | 8 ++++---- 11 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index 4f4861fc1..156728516 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -319,7 +319,7 @@ def _basis_CZ(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate == CNOT or gate == CX: + if gate == CX: qc_temp.add_gate( gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, @@ -351,7 +351,7 @@ def _basis_ISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate == CNOT or gate == CX: + if gate == CX: qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( @@ -412,7 +412,7 @@ def _basis_SQRTSWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate == CNOT or gate == CX: + if gate == CX: qc_temp.add_gate( gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, @@ -453,7 +453,7 @@ def _basis_SQRTISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate == CNOT or gate == CX: + if gate == CX: qc_temp.add_gate( gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 0a5edf54d..8b8093c91 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -607,7 +607,7 @@ def run_statistics(self, state, cbits=None): sim = CircuitSimulator(self, mode) return sim.run_statistics(state, cbits) - def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): + def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): """ Unitary matrix calculator for N qubits returning the individual steps as unitary matrices operating from left to right in the specified @@ -641,15 +641,7 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): measurements are added to the circuit") basis_1q_valid = ["RX", "RY", "RZ", "IDLE"] - basis_2q_valid = [ - "CNOT", - "CX", - "CSIGN", - "CZ", - "ISWAP", - "SQRTSWAP", - "SQRTISWAP", - ] + basis_2q_valid = ["CX", "CSIGN", "CZ", "ISWAP", "SQRTSWAP", "SQRTISWAP"] basis_1q = [] basis_2q = [] diff --git a/src/qutip_qip/circuit/draw/color_theme.py b/src/qutip_qip/circuit/draw/color_theme.py index d497bdba9..4c4f65c47 100644 --- a/src/qutip_qip/circuit/draw/color_theme.py +++ b/src/qutip_qip/circuit/draw/color_theme.py @@ -38,7 +38,6 @@ "SWAPALPHA": "#7648CB", # Dark Orchid "MS": "#7648CB", # Dark Orchid "RZX": "#7648CB", # Dark Orchid - "CNOT": "#9598F5", # Light Slate Blue "CX": "#9598F5", # Light Slate Blue "CY": "#9598F5", # Light Slate Blue "CZ": "#9598F5", # Light Slate Blue @@ -90,7 +89,6 @@ "SWAPALPHA": "#CDC1E8", # Light Purple "MS": "#CDC1E8", # Light Purple "RZX": "#CDC1E8", # Light Purple - "CNOT": "#E0E2F7", # Very Light Indigo "CX": "#E0E2F7", # Very Light Indigo "CY": "#E0E2F7", # Very Light Indigo "CZ": "#E0E2F7", # Very Light Indigo @@ -142,7 +140,6 @@ "SWAPALPHA": "#6A5ACD", # Slate Blue "MS": "#6A5ACD", # Slate Blue "RZX": "#6A5ACD", # Slate Blue - "CNOT": "#4682B4", # Steel Blue "CX": "#4682B4", # Steel Blue "CY": "#4682B4", # Steel Blue "CZ": "#4682B4", # Steel Blue @@ -195,7 +192,6 @@ "SWAPALPHA": "#4A5D6D", # Dark Slate Blue "MS": "#4A5D6D", # Dark Slate Blue "RZX": "#4A5D6D", # Dark Slate Blue - "CNOT": "#5D8AA8", # Medium Slate Blue "CX": "#5D8AA8", # Medium Slate Blue "CY": "#5D8AA8", # Medium Slate Blue "CZ": "#5D8AA8", # Medium Slate Blue diff --git a/src/qutip_qip/circuit/draw/mat_renderer.py b/src/qutip_qip/circuit/draw/mat_renderer.py index ba25b7ece..4c946c134 100644 --- a/src/qutip_qip/circuit/draw/mat_renderer.py +++ b/src/qutip_qip/circuit/draw/mat_renderer.py @@ -545,7 +545,7 @@ def _draw_multiq_gate( ) com_xskip = self._get_xskip(wire_list, layer) - if gate in [std.CNOT, std.CX]: + if gate == std.CX: self._draw_control_node(controls[0], com_xskip, self.color) self._draw_target_node(targets[0], com_xskip, self.color) self._draw_qbridge(targets[0], controls[0], com_xskip, self.color) diff --git a/src/qutip_qip/circuit/draw/texrenderer.py b/src/qutip_qip/circuit/draw/texrenderer.py index 9180ef050..81d99790d 100644 --- a/src/qutip_qip/circuit/draw/texrenderer.py +++ b/src/qutip_qip/circuit/draw/texrenderer.py @@ -99,7 +99,7 @@ def latex_code(self) -> str: rf" \ghost{{{self._gate_label(gate)}}} " ) - elif gate in [std.CNOT, std.CX]: + elif gate == std.CX: col.append(r" \targ ") elif gate == std.CY: col.append(r" \targ ") diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 03ca217fd..99f411d7d 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -90,7 +90,6 @@ def __init__(self, num_qubits, params): "RY": self.ry_compiler, "RX": self.rx_compiler, "CX": self.cnot_compiler, - "CNOT": self.cnot_compiler, "RZX": self.rzx_compiler, } ) diff --git a/src/qutip_qip/compiler/scheduler.py b/src/qutip_qip/compiler/scheduler.py index ad3ff562e..7882285ba 100644 --- a/src/qutip_qip/compiler/scheduler.py +++ b/src/qutip_qip/compiler/scheduler.py @@ -548,7 +548,7 @@ def commutation_rules(self, ind1, ind2, instructions): [instruction1, instruction2], key=lambda instruction: instruction.name, ) - if instruction1.name in ["CNOT", "CX"] and instruction2.name in ( + if instruction1.name == "CX" and instruction2.name in ( "X", "RX", ): @@ -556,7 +556,7 @@ def commutation_rules(self, ind1, ind2, instructions): commute = True else: commute = False - elif instruction1.name in ["CNOT", "CX"] and instruction2.name in ( + elif instruction1.name == "CX" and instruction2.name in ( "Z", "RZ", ): diff --git a/src/qutip_qip/device/circuitqed.py b/src/qutip_qip/device/circuitqed.py index 6f707bdf8..ba8871548 100644 --- a/src/qutip_qip/device/circuitqed.py +++ b/src/qutip_qip/device/circuitqed.py @@ -69,7 +69,7 @@ def __init__(self, num_qubits, dims=None, zz_crosstalk=False, **params): **params, ) super().__init__(model=model) - self.native_gates = ["RX", "RY", "CNOT", "CX", "RZX"] + self.native_gates = ["RX", "RY", "CX", "RZX"] self._default_compiler = SCQubitsCompiler self.pulse_mode = "continuous" diff --git a/src/qutip_qip/operations/gates/__init__.py b/src/qutip_qip/operations/gates/__init__.py index 1cfd97f8e..69a0a218a 100644 --- a/src/qutip_qip/operations/gates/__init__.py +++ b/src/qutip_qip/operations/gates/__init__.py @@ -89,7 +89,7 @@ "CRX": CRX, "CRY": CRY, "CRZ": CRZ, - "CNOT": CNOT, + "CNOT": CX, "CX": CX, "CY": CY, "CZ": CZ, diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 62202895f..85b993cea 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -935,7 +935,6 @@ def read_qasm(qasm_input, mode="default", version="2.0", strmode=False): "T": "t", "CRZ": "crz", "CX": "cx", - "CNOT": "cx", "TOFFOLI": "ccx", } diff --git a/src/qutip_qip/transpiler/chain.py b/src/qutip_qip/transpiler/chain.py index 760076359..356ac5ea4 100644 --- a/src/qutip_qip/transpiler/chain.py +++ b/src/qutip_qip/transpiler/chain.py @@ -42,7 +42,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls targets = circ_instruction.targets - if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: + if gate in [std.CX, std.CSIGN, std.CZ]: start = min([targets[0], controls[0]]) end = max([targets[0], controls[0]]) @@ -143,7 +143,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls if j < N - end - 2: - if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -158,7 +158,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) elif j == N - end - 2: - if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -173,7 +173,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) else: - if gate in [std.CNOT, std.CX, std.CSIGN, std.CZ]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=(end + targets[0]) % N, From 43ec9597b50587aa3c2da7eea550fac3b6b56376 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 21:56:25 +0530 Subject: [PATCH 105/117] Correct several isinstances --- src/qutip_qip/algorithms/qpe.py | 1 - src/qutip_qip/circuit/circuit.py | 8 ++++---- src/qutip_qip/device/utils.py | 2 +- src/qutip_qip/qasm.py | 6 +++--- src/qutip_qip/vqa.py | 1 - 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index 30c6764bc..079eff8ab 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -55,7 +55,6 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): for i in range(num_counting_qubits): qc.add_gate(H, targets=[i]) - # Gate.clear_cache("qpe") # Apply controlled-U gates with increasing powers for i in range(num_counting_qubits): power = 2 ** (num_counting_qubits - i - 1) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 8b8093c91..2876d9c6c 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -305,7 +305,7 @@ def add_gate( stacklevel=2, ) - if isinstance(gate, std.GLOBALPHASE): + if type(gate) is std.GLOBALPHASE: self.add_global_phase(gate.arg_value[0]) return @@ -730,7 +730,7 @@ def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): targets = circ_instruction.targets controls = circ_instruction.controls - if isinstance(gate, std.RX) and "RX" not in basis_1q: + if type(gate) is std.RX and "RX" not in basis_1q: qc_temp.add_gate( std.RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, @@ -744,7 +744,7 @@ def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): targets=targets, ) - elif isinstance(gate, std.RY) and "RY" not in basis_1q: + elif type(gate) is std.RY and "RY" not in basis_1q: qc_temp.add_gate( std.RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, @@ -758,7 +758,7 @@ def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): targets=targets, ) - elif isinstance(gate, std.RZ) and "RZ" not in basis_1q: + elif type(gate) is std.RZ and "RZ" not in basis_1q: qc_temp.add_gate( std.RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, diff --git a/src/qutip_qip/device/utils.py b/src/qutip_qip/device/utils.py index 1e363d5a6..34a6eb222 100644 --- a/src/qutip_qip/device/utils.py +++ b/src/qutip_qip/device/utils.py @@ -21,7 +21,7 @@ def _pulse_interpolate(pulse, tlist): coeff = np.zeros(len(tlist)) return coeff - if isinstance(pulse.coeff, bool): + if type(pulse.coeff) is bool: if pulse.coeff: coeff = np.ones(len(tlist)) else: diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 85b993cea..c5f9bc9fd 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -988,7 +988,7 @@ def _qasm_str(self, q_name, q_targets, q_controls=None, q_args=None): q_controls = [] q_regs = q_controls + q_targets - if isinstance(q_targets[0], int): + if type(q_targets[0]) is int: q_regs = ",".join([f"q[{reg}]" for reg in q_regs]) else: q_regs = ",".join(q_regs) @@ -1010,9 +1010,9 @@ def _qasm_defns(self, gate: Gate | Type[Gate]): QuTiP gate which needs to be defined in QASM format. """ - if isinstance(gate, gates.CRY): + if type(gate) is gates.CRY: gate_def = "gate cry(theta) a,b { cu3(theta,0,0) a,b; }" - elif isinstance(gate, gates.CRX): + elif type(gate) is gates.CRX: gate_def = "gate crx(theta) a,b { cu3(theta,-pi/2,pi/2) a,b; }" elif gate == gates.SQRTX: gate_def = "gate sqrtnot a {h a; u1(-pi/2) a; h a; }" diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index 4314db6f1..7b0d46001 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -134,7 +134,6 @@ def construct_circuit(self, angles): circ = QubitCircuit(self.num_qubits) i = 0 for layer_num in range(self.num_layers): - # Gate.clear_cache("vqa") for block in self.blocks: if block.initial and layer_num > 0: continue From 3f4cd43ccee89ed0c28fd2d14bc2a0a8e5b0ff60 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 22:01:18 +0530 Subject: [PATCH 106/117] Minor fixes as per recommendation --- src/qutip_qip/operations/controlled.py | 5 +++-- src/qutip_qip/operations/gateclass.py | 30 +++++++++++++++++--------- src/qutip_qip/operations/utils.py | 2 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index a0fc7bafd..3f81fad05 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -283,10 +283,11 @@ class _CustomControlledGate(ControlledGate): __slots__ = () namespace = gate_namespace name = gate_name - num_qubits = n_ctrl_qubits + gate.num_qubits + 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 - latex_str = r"{gate_name}" return _CustomControlledGate diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 5e5b3ed17..b93ecd5d2 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -47,18 +47,28 @@ def __init__(cls, name, bases, attrs): cls._is_frozen = True # Namespace being None corresponds to Temporary Gates - # Only if it is provided register it - if getattr(cls, "namespace", None) is not None: + # 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. + if (namespace.get(cls.name) is not None): + raise ValueError(f"Existing {cls.name} in namespace {namespace}") + + # 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) - # For lookup dictionary for Controlled Gates - # e.g. get_controlled_gate(X, num_ctrl_qubits=1, ctrl_value=1) this is CX, - # why do we have to define it again if it already exists. - if cls.is_controlled() and getattr(cls, "namespace", None) is not None: - cls.namespace.register( - (cls.target_gate.name, cls.num_ctrl_qubits, cls.ctrl_value), - cls, - ) def __setattr__(cls, name: str, value: any) -> None: """ diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py index d6059fbfa..b86c4e5ca 100644 --- a/src/qutip_qip/operations/utils.py +++ b/src/qutip_qip/operations/utils.py @@ -83,7 +83,7 @@ def _check_oper_dims( if oper.dims[0] != targ_dims: raise ValueError( f"The operator dims {oper.dims[0]} do not match " - "the target dims {targ_dims}." + f"the target dims {targ_dims}." ) From 25bd01a993c5af263c71a54616d709ce0e75a4d3 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Tue, 10 Mar 2026 22:32:13 +0530 Subject: [PATCH 107/117] Added more comments, docstring for gateclass --- src/qutip_qip/operations/gateclass.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index b93ecd5d2..ff7ea52b3 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -87,6 +87,7 @@ class X(Gate): """ # 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) @@ -145,16 +146,15 @@ def __init_subclass__(cls, **kwargs): """ 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. + 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. """ - super().__init_subclass__(**kwargs) - # Skip the below check for an abstract class if inspect.isabstract(cls): - return + 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): @@ -235,14 +235,17 @@ def __init_subclass__(cls, **kwargs): f"got {type(control_flag)} with value {control_flag}." ) + return super().__init_subclass__(**kwargs) + + def __init__(self) -> None: """ - This method is overwritten by Parametrized and Controlled Gates. + This method is overwritten in case of Parametrized and Controlled Gates. """ raise TypeError( - f"Gate '{type(self).name}' can't be initialized. " + f"Gate '{type(self).name}' can't be initialised. " f"If your gate requires parameters, it must inherit from 'ParametricGate'. " - f"Or if it must be controlled and needs control_value, it must inherit from 'ControlledGate'." + f"Or if it must be controlled, it must inherit from 'ControlledGate'." ) @staticmethod @@ -268,7 +271,7 @@ def inverse(cls) -> Type[Gate]: Returns ------- - Gate + Type[Gate] A Gate instance representing $G^{-1}$. """ if cls.self_inverse: From 8b78eacd26529313889e26480763faf527526d5f Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 04:24:51 +0530 Subject: [PATCH 108/117] Minor Fixes to gate class design --- src/qutip_qip/operations/controlled.py | 25 ++++++++++--------- src/qutip_qip/operations/gates/other_gates.py | 6 ++--- src/qutip_qip/operations/parametric.py | 16 ++++++++---- tests/test_compiler.py | 4 +-- tests/test_gates.py | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 3f81fad05..38ce9cb0f 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -1,8 +1,8 @@ import inspect import warnings -from typing import Type -from functools import partial from abc import abstractmethod +from functools import partial +from typing import Type from qutip import Qobj from qutip_qip.operations import ( @@ -115,6 +115,12 @@ def __init_subclass__(cls, **kwargs) -> None: f"Class '{cls.name}' method 'is_controlled()' must always return True." ) + if cls.is_parametric() != cls.target_gate.is_parametric(): + raise ValueError( + f"Class '{cls.name}' method 'is_parametric()' must return {cls.target_gate.is_parametric()}." + ) + + def __init__(self, *args, **kwargs) -> None: self._target_inst = self.target_gate(*args, **kwargs) @@ -181,14 +187,12 @@ def get_qobj(cls_or_self, dtype: str = "dense") -> Qobj: The unitary matrix representing the controlled operation. """ if isinstance(cls_or_self, type): - return controlled_gate_unitary( - U=cls_or_self.target_gate.get_qobj(dtype), - num_controls=cls_or_self.num_ctrl_qubits, - control_value=cls_or_self.ctrl_value, - ) - + target_gate = cls_or_self.target_gate + else: + target_gate = cls_or_self._target_inst + return controlled_gate_unitary( - U=cls_or_self._target_inst.get_qobj(dtype), + U=target_gate.get_qobj(dtype), num_controls=cls_or_self.num_ctrl_qubits, control_value=cls_or_self.ctrl_value, ) @@ -259,9 +263,6 @@ def get_controlled_gate( Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. """ - if gate_namespace is None: - gate_namespace = gate.namespace - if control_value is None: control_value = 2**n_ctrl_qubits - 1 diff --git a/src/qutip_qip/operations/gates/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py index 7185ffe87..ab8ca692e 100644 --- a/src/qutip_qip/operations/gates/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -31,12 +31,12 @@ def __init__(self, phase: float = 0.0): super().__init__(phase) def __repr__(self): - return f"Gate({self.name}, phase {self.arg_value[0]})" + return f"Gate({self.name}, phase {self.arg_value[0]}) -> Qobj:" - def compute_qobj(): + def compute_qobj(arg_value, dtype): raise NotImplementedError - def get_qobj(self, num_qubits=None, dtype: str = "dense"): + def get_qobj(self, num_qubits=None, dtype: str = "dense") -> Qobj: phase = self.arg_value[0] if num_qubits is None: return Qobj(phase, dtype=dtype) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index d4554cd3b..0ea27684c 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -64,8 +64,8 @@ def __init_subclass__(cls, **kwargs) -> None: f"got {type(num_params)} with value {num_params}." ) - # Validate params must take only one arguments args - # Inspect doesn't count self/cls as another argument + # Validate params must take only one argument 'args' + # Inspect doesn't count self/cls as an argument validate_params_func = getattr(cls, "validate_params") if len(inspect.signature(validate_params_func).parameters) > 1: raise SyntaxError( @@ -76,10 +76,10 @@ def __init_subclass__(cls, **kwargs) -> None: # compute_qobj method must take only two arguments arg_value, dtype compute_qobj_func = getattr(cls, "compute_qobj") - if len(inspect.signature(compute_qobj_func).parameters) > 2: + if len(inspect.signature(compute_qobj_func).parameters) != 2: raise SyntaxError( - f"Class '{cls.name}' method 'compute_qobj()' must take exactly 1 " - f"additional arguments (only the implicit 'arg_value, dtype')," + f"Class '{cls.name}' method 'compute_qobj()' must take exactly 2 " + f"arguments (only the implicit 'arg_value, dtype')," f" but it takes {len(inspect.signature(compute_qobj_func).parameters)}." ) @@ -88,6 +88,12 @@ def __init_subclass__(cls, **kwargs) -> None: f"Class '{cls.name}' method 'is_parametric()' must always return True." ) + if cls.is_controlled(): + raise ValueError( + f"Class '{cls.name}' method 'is_controlled()' must always return False." + ) + + def __init__(self, *args, arg_label: str | None = None): # This auto triggers a call to arg_value setter (where checks happen) self.arg_value = args diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 484af485f..594d0005a 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -85,7 +85,7 @@ class U1(AngleParametricGate): num_params = 1 self_inverse = False - def compute_qobj(self): + def compute_qobj(args, dtype): pass class U2(AngleParametricGate): @@ -93,7 +93,7 @@ class U2(AngleParametricGate): num_params = 1 self_inverse = False - def compute_qobj(): + def compute_qobj(args, dtype): pass num_qubits = 2 diff --git a/tests/test_gates.py b/tests/test_gates.py index 749bac6aa..564286227 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -621,7 +621,7 @@ class GoodParamGate(AngleParametricGate): num_params = 2 @staticmethod - def compute_qobj(args): + def compute_qobj(args, dtype): return qutip.qeye(2) with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): From c495ea28cfafdc55c7ea2678e95fbfa71c94d764 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 04:59:35 +0530 Subject: [PATCH 109/117] Added gate.name in pulse compiler with gate_class --- src/qutip_qip/compiler/cavityqedcompiler.py | 16 ++++++++-------- src/qutip_qip/compiler/circuitqedcompiler.py | 20 ++++++++++---------- src/qutip_qip/compiler/gatecompiler.py | 12 ++++++++---- src/qutip_qip/compiler/spinchaincompiler.py | 17 ++++++++++++----- tests/test_compiler.py | 10 +++++----- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 5a97f1785..4cd55e9c9 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -2,7 +2,7 @@ from qutip_qip.circuit import GateInstruction from qutip_qip.compiler import GateCompiler, PulseInstruction -from qutip_qip.operations.gates import RZ +from qutip_qip.operations.gates import RX, RZ, ISWAP, SQRTISWAP, GLOBALPHASE class CavityQEDCompiler(GateCompiler): @@ -75,10 +75,10 @@ def __init__( super().__init__(num_qubits, params=params, pulse_dict=pulse_dict, N=N) self.gate_compiler.update( { - "ISWAP": self.iswap_compiler, - "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, + ISWAP: self.iswap_compiler, + SQRTISWAP: self.sqrtiswap_compiler, + RZ: self.rz_compiler, + RX: self.rx_compiler, } ) self.wq = np.sqrt(self.params["eps"] ** 2 + self.params["delta"] ** 2) @@ -193,18 +193,18 @@ def _swap_compiler( ] # corrections - compiled_gate1 = self.gate_compiler["RZ"]( + compiled_gate1 = self.gate_compiler[RZ]( GateInstruction(operation=RZ(correction_angle), qubits=(q1,)), args, ) instruction_list += compiled_gate1 - compiled_gate2 = self.gate_compiler["RZ"]( + compiled_gate2 = self.gate_compiler[RZ]( GateInstruction(operation=RZ(correction_angle), qubits=(q2,)), args, ) instruction_list += compiled_gate2 - self.gate_compiler["GLOBALPHASE"](correction_angle) + self.gate_compiler[GLOBALPHASE](correction_angle) return instruction_list def sqrtiswap_compiler(self, circuit_instruction, args): diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 99f411d7d..6afcd3825 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -2,7 +2,7 @@ from qutip_qip.circuit import GateInstruction from qutip_qip.compiler import GateCompiler, PulseInstruction -from qutip_qip.operations.gates import RX, RY, RZX +from qutip_qip.operations.gates import CX, RX, RY, RZX class SCQubitsCompiler(GateCompiler): @@ -87,10 +87,10 @@ def __init__(self, num_qubits, params): super(SCQubitsCompiler, self).__init__(num_qubits, params=params) self.gate_compiler.update( { - "RY": self.ry_compiler, - "RX": self.rx_compiler, - "CX": self.cnot_compiler, - "RZX": self.rzx_compiler, + RY: self.ry_compiler, + RX: self.rx_compiler, + CX: self.cnot_compiler, + RZX: self.rzx_compiler, } ) self.args = { # Default configuration @@ -281,27 +281,27 @@ def cnot_compiler(self, circuit_instruction, args): q2 = circuit_instruction.targets[0] # += extends a list in Python - result += self.gate_compiler["RX"]( + result += self.gate_compiler[RX]( GateInstruction(operation=RX(-PI / 2), qubits=(q2,)), args, ) - result += self.gate_compiler["RZX"]( + result += self.gate_compiler[RZX]( GateInstruction(operation=RZX(PI / 2), qubits=(q1, q2)), args, ) - result += self.gate_compiler["RX"]( + result += self.gate_compiler[RX]( GateInstruction(operation=RX(-PI / 2), qubits=(q1,)), args, ) - result += self.gate_compiler["RY"]( + result += self.gate_compiler[RY]( GateInstruction(operation=RY(-PI / 2), qubits=(q1,)), args, ) - result += self.gate_compiler["RX"]( + result += self.gate_compiler[RX]( GateInstruction(operation=RX(PI / 2), qubits=(q1,)), args, ) diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 96f80d2d4..e41814036 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -4,6 +4,7 @@ from qutip_qip.compiler import PulseInstruction, Scheduler from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations.gates import GLOBALPHASE, IDLE class GateCompiler: @@ -61,8 +62,8 @@ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): self._num_qubits = num_qubits # backward compatibility self.params = params if params is not None else {} self.gate_compiler = { - "GLOBALPHASE": self.globalphase_compiler, - "IDLE": self.idle_compiler, + GLOBALPHASE: self.globalphase_compiler, + IDLE: self.idle_compiler, } self.args = { # Default configuration "shape": "rectangular", @@ -168,10 +169,13 @@ def compile(self, circuit, schedule_mode=None, args=None): # compile gates for circuit_instruction in instructions: gate = circuit_instruction.operation - if gate.name not in self.gate_compiler: + if gate.is_parametric(): + gate = type(gate) + + if gate not in self.gate_compiler: raise ValueError(f"Unsupported gate {gate.name}") - instruction = self.gate_compiler[gate.name]( + instruction = self.gate_compiler[gate]( circuit_instruction, self.args ) if instruction is None: diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index 4789a69b0..31ec02c90 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -1,6 +1,13 @@ import numpy as np from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.gates import ( + GLOBALPHASE, + ISWAP, + RX, + RZ, + SQRTISWAP, +) class SpinChainCompiler(GateCompiler): @@ -101,11 +108,11 @@ def __init__( super().__init__(num_qubits, params=params, pulse_dict=pulse_dict, N=N) self.gate_compiler.update( { - "ISWAP": self.iswap_compiler, - "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, - "GLOBALPHASE": self.globalphase_compiler, + ISWAP: self.iswap_compiler, + SQRTISWAP: self.sqrtiswap_compiler, + RZ: self.rz_compiler, + RX: self.rx_compiler, + GLOBALPHASE: self.globalphase_compiler, } ) self.global_phase = global_phase diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 594d0005a..101079355 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -56,8 +56,8 @@ def test_compiling_gates_different_sampling_number(): class MockCompiler(GateCompiler): def __init__(self, num_qubits, params=None): super().__init__(num_qubits, params=params) - self.gate_compiler["U1"] = self.single_qubit_gate_compiler - self.gate_compiler["U2"] = self.two_qubit_gate_compiler + self.gate_compiler[U1] = self.single_qubit_gate_compiler + self.gate_compiler[U2] = self.two_qubit_gate_compiler self.args.update({"params": params}) def single_qubit_gate_compiler(self, circuit_instruction, args): @@ -122,7 +122,7 @@ class MyCompiler(GateCompiler): # compiler class def __init__(self, num_qubits, params): super().__init__(num_qubits, params=params) # pass our compiler function as a compiler for RX (rotation around X) gate. - self.gate_compiler["RX"] = self.rx_compiler + self.gate_compiler[RX] = self.rx_compiler self.args.update({"params": params}) def rx_compiler(self, circuit_instruction, args): @@ -197,7 +197,7 @@ def test_compiler_without_pulse_dict(): compiler = SpinChainCompiler( num_qubits, params=processor.params, setup="circular" ) - compiler.gate_compiler["RX"] = rx_compiler_without_pulse_dict + compiler.gate_compiler[RX] = rx_compiler_without_pulse_dict compiler.args = {"params": processor.params} processor.load_circuit(circuit, compiler=compiler) result = processor.run_state(basis([2, 2], [0, 0])) @@ -229,7 +229,7 @@ def test_compiler_result_format(): assert_array_equal(processor.pulses[0].coeff, coeffs["sx0"]) assert_array_equal(processor.pulses[0].tlist, tlist["sx0"]) - compiler.gate_compiler["RX"] = rx_compiler_without_pulse_dict + compiler.gate_compiler[RX] = rx_compiler_without_pulse_dict tlist, coeffs = compiler.compile(circuit) assert type(tlist) is dict assert 0 in tlist From 03c9f69ee74fef592d706590db5bfe768679d2a9 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 06:30:14 +0530 Subject: [PATCH 110/117] Added CSdag, CTdag gates --- doc/source/qip-basics.rst | 2 + src/qutip_qip/circuit/draw/color_theme.py | 8 ++ src/qutip_qip/operations/gates/__init__.py | 6 ++ .../operations/gates/two_qubit_gate.py | 97 ++++++++++++++++++- 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/doc/source/qip-basics.rst b/doc/source/qip-basics.rst index b95ab81ad..074a31ca2 100644 --- a/doc/source/qip-basics.rst +++ b/doc/source/qip-basics.rst @@ -160,7 +160,9 @@ Gate name Description "CZ" Controlled Z gate "CH" Controlled H gate "CS" Controlled S gate +"CSdag" Controlled Sdag gate "CT" Controlled T gate +"CTdag" Controlled Tdag gate "CRX" Controlled rotation around x axis "CRY" Controlled rotation around y axis "CRZ" Controlled rotation around z axis diff --git a/src/qutip_qip/circuit/draw/color_theme.py b/src/qutip_qip/circuit/draw/color_theme.py index 4c4f65c47..c8e998f85 100644 --- a/src/qutip_qip/circuit/draw/color_theme.py +++ b/src/qutip_qip/circuit/draw/color_theme.py @@ -43,7 +43,9 @@ "CZ": "#9598F5", # Light Slate Blue "CH": "#9598F5", # Light Slate Blue "CS": "#9598F5", # Light Slate Blue + "CSdag": "#9598F5", # Light Slate Blue "CT": "#9598F5", # Light Slate Blue + "CTdag": "#9598F5", # Light Slate Blue "CRX": "#A66DDF", # Medium Purple "CRY": "#A66DDF", # Medium Purple "CRZ": "#A66DDF", # Medium Purple @@ -94,7 +96,9 @@ "CZ": "#E0E2F7", # Very Light Indigo "CH": "#E0E2F7", # Very Light Indigo "CS": "#E0E2F7", # Very Light Indigo + "CSdag": "#E0E2F7", # Very Light Indigo "CT": "#E0E2F7", # Very Light Indigo + "CTdag": "#E0E2F7", # Very Light Indigo "CRX": "#D6C9E8", # Light Muted Purple "CRY": "#D6C9E8", # Light Muted Purple "CRZ": "#D6C9E8", # Light Muted Purple @@ -145,7 +149,9 @@ "CZ": "#4682B4", # Steel Blue "CH": "#4682B4", # Steel Blue "CS": "#4682B4", # Steel Blue + "CSdag": "#4682B4", # Steel Blue "CT": "#4682B4", # Steel Blue + "CTdag": "#4682B4", # Steel Blue "CRX": "#7B68EE", # Medium Slate Blue "CRY": "#7B68EE", # Medium Slate Blue "CRZ": "#7B68EE", # Medium Slate Blue @@ -197,7 +203,9 @@ "CZ": "#5D8AA8", # Medium Slate Blue "CH": "#5D8AA8", # Medium Slate Blue "CS": "#5D8AA8", # Medium Slate Blue + "CSdag": "#5D8AA8", # Medium Slate Blue "CT": "#5D8AA8", # Medium Slate Blue + "CTdag": "#5D8AA8", # Medium Slate Blue "CRX": "#6C5B7B", # Dark Lavender "CRY": "#6C5B7B", # Dark Lavender "CRZ": "#6C5B7B", # Dark Lavender diff --git a/src/qutip_qip/operations/gates/__init__.py b/src/qutip_qip/operations/gates/__init__.py index 69a0a218a..eb8e122f6 100644 --- a/src/qutip_qip/operations/gates/__init__.py +++ b/src/qutip_qip/operations/gates/__init__.py @@ -37,7 +37,9 @@ CY, CZ, CS, + CSdag, CT, + CTdag, CH, CRX, CRY, @@ -94,7 +96,9 @@ "CY": CY, "CZ": CZ, "CS": CS, + "CSdag": CSdag, "CT": CT, + "CTdag": CTdag, "CH": CH, "CPHASE": CPHASE, "RZX": RZX, @@ -158,7 +162,9 @@ "CZ", "CH", "CS", + "CSdag", "CT", + "CTdag", "CPHASE", "RZX", "CQASMU", diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 71505fce6..d5ec9f354 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -11,7 +11,9 @@ Z, H, S, + Sdag, T, + Tdag, RX, RY, RZ, @@ -637,6 +639,7 @@ class CX(_ControlledTwoQubitGate): __slots__ = () target_gate: Final[Type[Gate]] = X + self_inverse: Final[bool] = True is_clifford: Final[bool] = True latex_str: Final[str] = r"{\rm CNOT}" @@ -671,8 +674,9 @@ class CY(_ControlledTwoQubitGate): __slots__ = () - is_clifford: Final[bool] = True target_gate: Final[Type[Gate]] = Y + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True latex_str: Final[str] = r"{\rm CY}" @staticmethod @@ -704,6 +708,7 @@ class CZ(_ControlledTwoQubitGate): __slots__ = () target_gate: Final[Type[Gate]] = Z + self_inverse: Final[bool] = True is_clifford: Final[bool] = True latex_str: Final[str] = r"{\rm CZ}" @@ -741,6 +746,7 @@ class CH(_ControlledTwoQubitGate): __slots__ = () target_gate: Final[Type[Gate]] = H + self_inverse: Final[bool] = True latex_str: Final[str] = r"{\rm CH}" @staticmethod @@ -796,6 +802,52 @@ def get_qobj(dtype: str = "dense") -> Qobj: dtype=dtype, ) + @staticmethod + def inverse() -> Type[Gate]: + return CTdag + + +class CTdag(_ControlledTwoQubitGate): + r""" + CTdag gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CTdag + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Tdag + latex_str: Final[str] = r"{\rm CT^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, (1 - 1j) / np.sqrt(2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CT + class CS(_ControlledTwoQubitGate): r""" @@ -831,6 +883,49 @@ def get_qobj(dtype: str = "dense") -> Qobj: dtype=dtype, ) + @staticmethod + def inverse() -> Type[Gate]: + return CSdag + + +class CSdag(_ControlledTwoQubitGate): + r""" + CS gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CS + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Sdag + latex_str: Final[str] = r"{\rm CS^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1j]] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CS + class CRX(_ControlledTwoQubitGate): r""" From db49f475a58eec9200e9674ef5966cd6d0507e57 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 06:35:43 +0530 Subject: [PATCH 111/117] Removed expanded from inverse() in Parametric Gates --- src/qutip_qip/operations/controlled.py | 8 +-- .../operations/gates/single_qubit_gate.py | 50 +++---------------- .../operations/gates/two_qubit_gate.py | 25 ++-------- src/qutip_qip/operations/parametric.py | 2 +- 4 files changed, 17 insertions(+), 68 deletions(-) diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 38ce9cb0f..6ba1ceccc 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -211,15 +211,15 @@ def inverse(cls_or_self) -> Gate | Type[Gate]: ) else: - inverse_gate_class, param = cls_or_self._target_inst.inverse( - expanded=True - ) + 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, - )(*param) + )(*params) return inverse_gate diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 5337ae537..5f2accb36 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -383,13 +383,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: theta = self.arg_value[0] - if expanded: - return RX, (-theta,) return RX(-theta) @@ -428,13 +423,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: theta = self.arg_value[0] - if expanded: - return RY, (-theta,) return RY(-theta) @@ -470,13 +460,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: theta = self.arg_value[0] - if expanded: - return RZ, (-theta,) return RZ(-theta) @@ -507,13 +492,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: theta = self.arg_value[0] - if expanded: - return PHASE, (-theta,) return PHASE(-theta) @@ -565,16 +545,9 @@ def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float, float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float]]: phi, theta = self.arg_value - inverse_params = (phi, -theta) - - if expanded: - return R, inverse_params - return R(*inverse_params) + return R(phi, -theta) class QASMU(_SingleQubitParametricGate): @@ -627,13 +600,6 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: theta, phi, gamma = self.arg_value - inverse_param = (-theta, -gamma, -phi) - - if expanded: - return QASMU, inverse_param - return QASMU(*inverse_param) + return QASMU(-theta, -gamma, -phi) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index d5ec9f354..94b4d5a37 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -480,13 +480,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: alpha = self.arg_value[0] - if expanded: - return SWAPALPHA, (-alpha,) return SWAPALPHA(-alpha) @@ -548,16 +543,9 @@ def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float, float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float]]: theta, phi = self.arg_value - inverse_param = (-theta, phi) - - if expanded: - return MS, inverse_param - return MS(*inverse_param) + return MS(-theta, phi) class RZX(_TwoQubitParametricGate): @@ -610,13 +598,8 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: dtype=dtype, ) - def inverse( - self, expanded: bool = False - ) -> Gate | tuple[Type[Gate], tuple[float]]: - + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: theta = self.arg_value[0] - if expanded: - return RZX, (-theta,) return RZX(-theta) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 0ea27684c..377af3369 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -148,7 +148,7 @@ def get_qobj(self, dtype: str = "dense") -> Qobj: def compute_qobj(args: tuple, dtype: str) -> Qobj: raise NotImplementedError - def inverse(self, expanded: bool = False) -> Gate: + def inverse(self) -> Gate: if self.self_inverse: return self raise NotImplementedError From 6f0b7cf408bb7137575870a69fcb0b8db54f0311 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 06:39:21 +0530 Subject: [PATCH 112/117] Add inverse for standard controlled parametric gates --- .../operations/gates/two_qubit_gate.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py index 94b4d5a37..c2c2c2c67 100644 --- a/src/qutip_qip/operations/gates/two_qubit_gate.py +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -946,6 +946,10 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: def get_qobj(self, dtype: str = "dense") -> Qobj: return self.compute_qobj(self.arg_value, dtype=dtype) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRX(-theta) + class CRY(_ControlledTwoQubitGate): r""" @@ -983,6 +987,10 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: def get_qobj(self, dtype: str = "dense") -> Qobj: return self.compute_qobj(self.arg_value, dtype) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRY(-theta) + class CRZ(_ControlledTwoQubitGate): r""" @@ -1036,6 +1044,10 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: def get_qobj(self, dtype: str = "dense") -> Qobj: return self.compute_qobj(self.arg_value, dtype) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRZ(-theta) + class CPHASE(_ControlledTwoQubitGate): r""" @@ -1089,6 +1101,10 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: def get_qobj(self, dtype: str = "dense") -> Qobj: return self.compute_qobj(self.arg_value, dtype) + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CPHASE(-theta) + class CQASMU(_ControlledTwoQubitGate): r""" @@ -1143,3 +1159,7 @@ def compute_qobj( def get_qobj(self, dtype: str = "dense") -> Qobj: return self.compute_qobj(self.arg_value, dtype=dtype) + + def inverse(self) -> Gate: + theta, phi, gamma = self.arg_value + return CQASMU(-theta, -gamma, -phi) From aae676abbaa4aa4c9ca3555fadf6bdb2184742f8 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 07:15:55 +0530 Subject: [PATCH 113/117] Remove expanded from Parametric Gates --- src/qutip_qip/operations/parametric.py | 1 - tests/test_gates.py | 37 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index 377af3369..d3967e7be 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -65,7 +65,6 @@ def __init_subclass__(cls, **kwargs) -> None: ) # Validate params must take only one argument 'args' - # Inspect doesn't count self/cls as an argument validate_params_func = getattr(cls, "validate_params") if len(inspect.signature(validate_params_func).parameters) > 1: raise SyntaxError( diff --git a/tests/test_gates.py b/tests/test_gates.py index 564286227..e930f1963 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -20,6 +20,7 @@ ) import qutip_qip.operations.gates as gates +rng = np.random.default_rng(seed=101) def _permutation_id(permutation): return str(len(permutation)) + "-" + "".join(map(str, permutation)) @@ -401,6 +402,37 @@ def test_dtype(self): assert isinstance(expanded_qobj, qutip.data.Dense) +rand_U = qutip.rand_unitary(dimensions=[2], seed=rng) + +class U1(Gate): + num_qubits = 1 + + @staticmethod + def get_qobj(dtype: str = "dense"): + return rand_U.to(dtype) + + @staticmethod + def inverse(): + return get_unitary_gate("U1_dag", rand_U.dag()) + + +class U2(AngleParametricGate): + num_qubits = 1 + num_params = 1 + + @staticmethod + def compute_qobj(args, dtype: str = "dense"): + theta = args[0] + return qutip.Qobj([ + [np.exp(-1j * theta), 0], + [0, np.exp(1j * theta)], + ]) + + def inverse(self): + theta = self.arg_value[0] + return U2(-theta) + + GATES = [ gates.X, gates.Y, @@ -419,6 +451,7 @@ def test_dtype(self): gates.SQRTISWAPdag, gates.BERKELEY, gates.BERKELEYdag, + U1, ] PARAMETRIC_GATE = [ @@ -431,6 +464,7 @@ def test_dtype(self): gates.SWAPALPHA(0.3), gates.MS(0.47, 0.8), gates.RZX(0.6), + U2(0.447), ] CONTROLLED_GATE = [ @@ -447,6 +481,8 @@ def test_dtype(self): gates.CQASMU(0.9, 0.22, 0.15), gates.TOFFOLI, gates.FREDKIN, + get_controlled_gate(U1, 1, 1), + get_controlled_gate(U2, 1, 1)(0.88), ] @@ -454,6 +490,7 @@ def test_dtype(self): def test_gate_inverse(gate: Gate | Type[Gate]): n = 2**gate.num_qubits inverse = gate.inverse() + print(rand_U) np.testing.assert_allclose( (gate.get_qobj() * inverse.get_qobj()).full(), np.eye(n), From dd2b5eaf48500eea2ce9a26a96837e125c8d4fe2 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 07:16:24 +0530 Subject: [PATCH 114/117] run black --- src/qutip_qip/circuit/circuit.py | 9 ++++++++- src/qutip_qip/operations/controlled.py | 3 +-- src/qutip_qip/operations/gateclass.py | 18 +++++++++++------- src/qutip_qip/operations/parametric.py | 1 - tests/test_gates.py | 16 +++++++++++----- tests/test_renderer.py | 4 +++- 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 2876d9c6c..c4c22e8e6 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -641,7 +641,14 @@ def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): measurements are added to the circuit") basis_1q_valid = ["RX", "RY", "RZ", "IDLE"] - basis_2q_valid = ["CX", "CSIGN", "CZ", "ISWAP", "SQRTSWAP", "SQRTISWAP"] + basis_2q_valid = [ + "CX", + "CSIGN", + "CZ", + "ISWAP", + "SQRTSWAP", + "SQRTISWAP", + ] basis_1q = [] basis_2q = [] diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py index 6ba1ceccc..ea7dd7f3e 100644 --- a/src/qutip_qip/operations/controlled.py +++ b/src/qutip_qip/operations/controlled.py @@ -120,7 +120,6 @@ def __init_subclass__(cls, **kwargs) -> None: f"Class '{cls.name}' method 'is_parametric()' must return {cls.target_gate.is_parametric()}." ) - def __init__(self, *args, **kwargs) -> None: self._target_inst = self.target_gate(*args, **kwargs) @@ -190,7 +189,7 @@ def get_qobj(cls_or_self, dtype: str = "dense") -> Qobj: 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, diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index ff7ea52b3..25847e43c 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -49,27 +49,32 @@ def __init__(cls, name, bases, attrs): # 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. - if (namespace.get(cls.name) is not None): - raise ValueError(f"Existing {cls.name} in namespace {namespace}") + if namespace.get(cls.name) is not None: + raise ValueError( + f"Existing {cls.name} in namespace {namespace}" + ) # 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.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 @@ -237,7 +242,6 @@ def __init_subclass__(cls, **kwargs): return super().__init_subclass__(**kwargs) - def __init__(self) -> None: """ This method is overwritten in case of Parametrized and Controlled Gates. diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index d3967e7be..b16cfb0aa 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -92,7 +92,6 @@ def __init_subclass__(cls, **kwargs) -> None: f"Class '{cls.name}' method 'is_controlled()' must always return False." ) - def __init__(self, *args, arg_label: str | None = None): # This auto triggers a call to arg_value setter (where checks happen) self.arg_value = args diff --git a/tests/test_gates.py b/tests/test_gates.py index e930f1963..aa77427bc 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -22,6 +22,7 @@ rng = np.random.default_rng(seed=101) + def _permutation_id(permutation): return str(len(permutation)) + "-" + "".join(map(str, permutation)) @@ -404,6 +405,7 @@ def test_dtype(self): rand_U = qutip.rand_unitary(dimensions=[2], seed=rng) + class U1(Gate): num_qubits = 1 @@ -423,10 +425,12 @@ class U2(AngleParametricGate): @staticmethod def compute_qobj(args, dtype: str = "dense"): theta = args[0] - return qutip.Qobj([ - [np.exp(-1j * theta), 0], - [0, np.exp(1j * theta)], - ]) + return qutip.Qobj( + [ + [np.exp(-1j * theta), 0], + [0, np.exp(1j * theta)], + ] + ) def inverse(self): theta = self.arg_value[0] @@ -788,7 +792,9 @@ def test_standard_gates_failures(self): get_controlled_gate(gates.X, n_ctrl_qubits=1, control_value=2) with pytest.raises(TypeError): - get_controlled_gate(gates.X, n_ctrl_qubits=0) # num_ctrl_qubits > 0 + get_controlled_gate( + gates.X, n_ctrl_qubits=0 + ) # num_ctrl_qubits > 0 def test_class_attribute_modification(self): with pytest.raises(AttributeError): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index cd27f492c..6c7e472c9 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -166,7 +166,9 @@ def qc3(): def qc4(): i = get_controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="i") ii = get_controlled_gate(IDLE, n_ctrl_qubits=2, gate_name="ii") - iii = get_controlled_gate(IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii") + iii = get_controlled_gate( + IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii" + ) qc = QubitCircuit(5, num_cbits=2) qc.add_gate( From 4378e1fe6e505951bda7a59e65d78d7bdab73eb7 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 07:19:20 +0530 Subject: [PATCH 115/117] Added a default inverse method for non-parametrized gates --- src/qutip_qip/operations/gateclass.py | 2 +- tests/test_gates.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 25847e43c..56f6ced1a 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -280,7 +280,7 @@ def inverse(cls) -> Type[Gate]: """ if cls.self_inverse: return cls - raise NotImplementedError + return get_unitary_gate("Test", cls.get_qobj().dag()) @staticmethod def is_controlled() -> bool: diff --git a/tests/test_gates.py b/tests/test_gates.py index aa77427bc..683108bfc 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -413,10 +413,6 @@ class U1(Gate): def get_qobj(dtype: str = "dense"): return rand_U.to(dtype) - @staticmethod - def inverse(): - return get_unitary_gate("U1_dag", rand_U.dag()) - class U2(AngleParametricGate): num_qubits = 1 @@ -589,9 +585,6 @@ def get_qobj(): # For a given gateclass, class attribute like num_qubit can't be modified GoodGate.num_qubits = 2 - with pytest.raises(NotImplementedError): - GoodGate.inverse() - U_rect = qutip.Qobj(np.eye(2, 3)) with pytest.raises(ValueError): get_unitary_gate("U_rect", U_rect) # U must be a square matrix From 0b1cd2f074a2d3607bde348783fb2904ad7ebd79 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 08:39:30 +0530 Subject: [PATCH 116/117] Separated Identity and IDLE gate --- doc/source/qip-basics.rst | 2 +- src/qutip_qip/circuit/circuit.py | 2 +- src/qutip_qip/circuit/draw/color_theme.py | 4 ++ src/qutip_qip/operations/gateclass.py | 4 +- src/qutip_qip/operations/gates/__init__.py | 3 ++ .../operations/gates/single_qubit_gate.py | 39 +++++++++++++++++-- src/qutip_qip/operations/parametric.py | 10 ++--- src/qutip_qip/qasm.py | 2 +- tests/test_device.py | 2 +- tests/test_renderer.py | 8 ++-- 10 files changed, 57 insertions(+), 19 deletions(-) diff --git a/doc/source/qip-basics.rst b/doc/source/qip-basics.rst index 074a31ca2..b2b509ca1 100644 --- a/doc/source/qip-basics.rst +++ b/doc/source/qip-basics.rst @@ -183,7 +183,7 @@ Gate name Description "TOFFOLI" (CCX) Toffoli gate "FREDKIN" Fredkin gate "GLOBALPHASE" Global phase gate -"IDLE" Identity gate +"IDENTITY" Identity gate ==================== ======================================== For some of the gates listed above, :class:`.QubitCircuit` also has a primitive :func:`.QubitCircuit.resolve_gates()` method that decomposes them into elementary gate sets such as CX or SWAP with single-qubit gates (RX, RY and RZ). However, this method is not fully optimized. It is very likely that the depth of the circuit can be further reduced by merging quantum gates. It is required that the gate resolution be carried out before the measurements to the circuit are added. diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index c4c22e8e6..1c31f1c08 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -640,7 +640,7 @@ def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): raise NotImplementedError("adjacent_gates must be called before \ measurements are added to the circuit") - basis_1q_valid = ["RX", "RY", "RZ", "IDLE"] + basis_1q_valid = ["RX", "RY", "RZ", "IDENTITY"] basis_2q_valid = [ "CX", "CSIGN", diff --git a/src/qutip_qip/circuit/draw/color_theme.py b/src/qutip_qip/circuit/draw/color_theme.py index c8e998f85..320a8523f 100644 --- a/src/qutip_qip/circuit/draw/color_theme.py +++ b/src/qutip_qip/circuit/draw/color_theme.py @@ -7,6 +7,7 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#000000", # Black + "IDENTITY": "#FFFFFF", # White "IDLE": "#FFFFFF", # White "X": "#CB4BF9", # Medium Orchid "Y": "#CB4BF9", # Medium Orchid @@ -60,6 +61,7 @@ "color": "#000000", # Black "wire_color": "#000000", # Black "default_gate": "#D8CDAF", # Bit Dark Beige + "IDENTITY": "#FFFFFF", # White "IDLE": "#FFFFFF", # White "X": "#F4A7B9", # Light Pink "Y": "#F4A7B9", # Light Pink @@ -113,6 +115,7 @@ "color": "#000000", # Black "wire_color": "#989898", # Dark Gray "default_gate": "#D8BFD8", # (Thistle) + "IDENTITY": "#FFFFFF", # White "IDLE": "#FFFFFF", # White "X": "#9370DB", # Medium Purple "Y": "#9370DB", # Medium Purple @@ -167,6 +170,7 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#ED9455", # Slate Orange + "IDENTITY": "#FFFFFF", # White "IDLE": "#FFFFFF", # White "X": "#4A5D6D", # Dark Slate Blue "Y": "#4A5D6D", # Dark Slate Blue diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 56f6ced1a..3a1ad83de 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -147,7 +147,7 @@ class attribute for subclasses. is_clifford: bool = False latex_str: str - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs) -> None: """ Automatically runs when a new subclass is defined via inheritance. @@ -280,7 +280,7 @@ def inverse(cls) -> Type[Gate]: """ if cls.self_inverse: return cls - return get_unitary_gate("Test", cls.get_qobj().dag()) + return get_unitary_gate(f"{cls.name}_inv", cls.get_qobj().dag()) @staticmethod def is_controlled() -> bool: diff --git a/src/qutip_qip/operations/gates/__init__.py b/src/qutip_qip/operations/gates/__init__.py index eb8e122f6..2a47dc0e2 100644 --- a/src/qutip_qip/operations/gates/__init__.py +++ b/src/qutip_qip/operations/gates/__init__.py @@ -17,6 +17,7 @@ Tdag, R, QASMU, + IDENTITY, IDLE, ) from .two_qubit_gate import ( @@ -56,6 +57,7 @@ GATE_CLASS_MAP = { "GLOBALPHASE": GLOBALPHASE, + "IDENTITY": IDENTITY, "IDLE": IDLE, "X": X, "Y": Y, @@ -122,6 +124,7 @@ __all__ = [ "GATE_CLASS_MAP", + "IDENTITY", "IDLE", "X", "Y", diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 5f2accb36..456a94ef2 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -6,6 +6,7 @@ from qutip import Qobj, sigmax, sigmay, sigmaz, qeye from qutip_qip.operations import Gate, AngleParametricGate from qutip_qip.operations.namespace import NS_GATE, NameSpace +from qutip_qip.typing import Real class _SingleQubitGate(Gate): @@ -102,20 +103,20 @@ def get_qobj(dtype: str = "dense") -> Qobj: return sigmaz(dtype=dtype) -class IDLE(_SingleQubitGate): +class IDENTITY(_SingleQubitGate): """ - IDLE gate. + IDENTITY gate. Examples -------- - >>> from qutip_qip.operations.gates import IDLE + >>> from qutip_qip.operations.gates import IDENTITY """ __slots__ = () self_inverse: Final[bool] = True is_clifford: Final[bool] = True - latex_str: Final[str] = r"{\rm IDLE}" + latex_str: Final[str] = r"{\rm I}" @staticmethod @cache @@ -603,3 +604,33 @@ def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: theta, phi, gamma = self.arg_value return QASMU(-theta, -gamma, -phi) + +class IDLE(AngleParametricGate): + """ + IDLE gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import IDLE + """ + + __slots__ = () + num_qubits = 1 + num_params = 1 + + def __init__(self, T: float, arg_label=None): + super().__init__(T, arg_label=arg_label) + + @staticmethod + def validate_params(args): + if not isinstance(args[0], Real): + raise TypeError(f"{args[0]} must be a float") + if args[0] < 0: + raise ValueError(f"IDLE time must be non-negative, got {args[0]}") + + @staticmethod + def compute_qobj(args, dtype: str = "dense") -> Qobj: + # Practically not required as this gate is only useful in pulse level + # simulation, and the pulse compiler implementation of it will be + # independent of get_qobj() + return qeye(2, dtype=dtype) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py index b16cfb0aa..6ac864f60 100644 --- a/src/qutip_qip/operations/parametric.py +++ b/src/qutip_qip/operations/parametric.py @@ -92,7 +92,7 @@ def __init_subclass__(cls, **kwargs) -> None: f"Class '{cls.name}' method 'is_controlled()' must always return False." ) - def __init__(self, *args, arg_label: str | None = None): + def __init__(self, *args, arg_label: str | None = None) -> None: # This auto triggers a call to arg_value setter (where checks happen) self.arg_value = args self.arg_label = arg_label @@ -102,7 +102,7 @@ def arg_value(self) -> tuple[any, ...]: return self._arg_value @arg_value.setter - def arg_value(self, new_args: Sequence): + def arg_value(self, new_args: Sequence) -> None: if not isinstance(new_args, Sequence): new_args = [new_args] @@ -152,10 +152,10 @@ def inverse(self) -> Gate: raise NotImplementedError @staticmethod - def is_parametric(): + def is_parametric() -> bool: return True - def __str__(self): + def __str__(self) -> str: return f""" Gate({self.name}, arg_value={self.arg_value}, arg_label={self.arg_label}), @@ -180,7 +180,7 @@ class AngleParametricGate(ParametricGate): __slots__ = () @staticmethod - def validate_params(arg_value): + def validate_params(arg_value) -> None: for arg in arg_value: if not isinstance(arg, Real): raise TypeError(f"Invalid arg {arg} in arg_value") diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index c5f9bc9fd..5dc65aafb 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -565,7 +565,7 @@ def _add_qiskit_gates( ) elif name == "id": qc.add_gate( - gates.IDLE, + gates.IDENTITY, targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, diff --git a/tests/test_device.py b/tests/test_device.py index dc6ba687a..fd242cca5 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -188,7 +188,7 @@ def _test_numerical_evolution_helper( circuit.add_gate(ISWAP, targets=[2, 1]) circuit.add_gate(Y, targets=[2]) circuit.add_gate(Z, targets=[0]) -circuit.add_gate(IDLE, targets=[1]) +circuit.add_gate(IDLE(0), targets=[1]) circuit.add_gate(CX, targets=[0], controls=[2]) circuit.add_gate(Z, targets=[1]) circuit.add_gate(X, targets=[1]) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 6c7e472c9..4c958d044 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -5,7 +5,7 @@ from qutip_qip.circuit.draw import TextRenderer from qutip_qip.operations import get_controlled_gate from qutip_qip.operations.gates import ( - IDLE, + IDENTITY, X, H, CX, @@ -164,10 +164,10 @@ def qc3(): @pytest.fixture def qc4(): - i = get_controlled_gate(IDLE, n_ctrl_qubits=1, gate_name="i") - ii = get_controlled_gate(IDLE, n_ctrl_qubits=2, gate_name="ii") + i = get_controlled_gate(IDENTITY, n_ctrl_qubits=1, gate_name="i") + ii = get_controlled_gate(IDENTITY, n_ctrl_qubits=2, gate_name="ii") iii = get_controlled_gate( - IDLE, n_ctrl_qubits=1, control_value=0, gate_name="iii" + IDENTITY, n_ctrl_qubits=1, control_value=0, gate_name="iii" ) qc = QubitCircuit(5, num_cbits=2) From 5c7ac6453018f6550e43c79cf553340a3d626ab1 Mon Sep 17 00:00:00 2001 From: Mayank Goel Date: Wed, 11 Mar 2026 08:50:30 +0530 Subject: [PATCH 117/117] Add docstrings for namespace --- .../operations/gates/single_qubit_gate.py | 3 +- src/qutip_qip/operations/namespace.py | 104 ++++++++++++++++-- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py index 456a94ef2..13cad7e89 100644 --- a/src/qutip_qip/operations/gates/single_qubit_gate.py +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -605,6 +605,7 @@ def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: theta, phi, gamma = self.arg_value return QASMU(-theta, -gamma, -phi) + class IDLE(AngleParametricGate): """ IDLE gate. @@ -633,4 +634,4 @@ def compute_qobj(args, dtype: str = "dense") -> Qobj: # Practically not required as this gate is only useful in pulse level # simulation, and the pulse compiler implementation of it will be # independent of get_qobj() - return qeye(2, dtype=dtype) + return qeye(2, dtype=dtype) diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py index 50a62baf6..bf0e75ec9 100644 --- a/src/qutip_qip/operations/namespace.py +++ b/src/qutip_qip/operations/namespace.py @@ -6,7 +6,8 @@ class _SingletonMeta(type): """ - Note this is not a thread-safe implementation of Singleton. + Metaclass to implement the Singleton design pattern. + Note that this is not a thread-safe implementation of a Singleton. """ _instances = {} @@ -19,21 +20,42 @@ def __call__(cls, *args, **kwargs): class _GlobalNameSpaceRegistry(metaclass=_SingletonMeta): + """ + Global registry to manage and store all active namespaces. + + This class enforces a singleton pattern (using the metaclass) to ensure that only one global + registry exists during the runtime of the application. + """ + def __init__(self): self._registry: set[NameSpace] = set() def register_namespace(self, namespace: NameSpace) -> None: - """Safely adds an item to the specific namespace.""" + """ + Safely adds a new namespace to the global registry. + + Note: This means that a gate (or operation) is never garbage + collected until the Namespace is destroyed. This is the desired + behavior for standard library gates. + + Parameters + ---------- + namespace : NameSpace + The namespace instance to be registered. + + Raises + ------ + ValueError + If the namespace already exists within the registry. + """ if namespace in self._registry: raise ValueError(f"Existing namespace {namespace}") - # Note: This does mean that gate (or operation) is never garbage - # collected until the Namespace exists. This is fine for standard gates. self._registry.add(namespace) # Default behaviour for user defining his own gates is that namespace is None, # Thus those gates are considered temporary by default, we use the same logic in - # QPE for Controlled Unitary gates, VQA (until Ops i.e. composite gates are introduced). + # QPE for Controlled Unitary gates, VQA untils Ops are implemented. _GlobalRegistry = _GlobalNameSpaceRegistry() @@ -41,11 +63,31 @@ def register_namespace(self, namespace: NameSpace) -> None: @dataclass class NameSpace: + """ + Represents a distinct, optionally hierarchical namespace for registering + quantum operations. + + Parameters + ---------- + local_name : str + The local identifier for the namespace. Must not contain periods ('.'). + parent : NameSpace or None, optional + The parent namespace, if this is a nested sub-namespace. Default is None. + """ + local_name: str parent: NameSpace | None = None _registry: dict[str, any] = field(default_factory=dict) def __post_init__(self): + """ + Validates the namespace name and registers it globally upon creation. + + Raises + ------ + ValueError + If `local_name` contains a dot, as dots are reserved for hierarchy. + """ if "." in self.local_name: raise ValueError( f"Namespace local_name '{self.local_name}' cannot contain dots. " @@ -55,6 +97,10 @@ def __post_init__(self): @cached_property def name(self) -> str: + """ + str: The fully qualified, hierarchical name of the namespace. + (e.g., 'std.gates'). + """ if self.parent: return f"{self.parent.name}.{self.local_name}" return self.local_name @@ -62,9 +108,22 @@ def name(self) -> str: def register( self, name: str | tuple[str, int, int], operation_cls: any ) -> None: - """Safely adds an item to the specific namespace. - name is str for a non-controlled Gate - name is a tuple (target_gate.name, num_ctrl_qubits, ctrl_values) for a Controlled gate. + """ + Safely adds an item to this specific namespace. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier for the operation. Use a string for a non-controlled + gate. Use a tuple `(target_gate.name, num_ctrl_qubits, ctrl_values)` + as a second key for controlled gates. + operation_cls : any + The operation class or object to register. + + Raises + ------ + NameError + If an operation with the given name already exists in this namespace. """ if name in self._registry: raise NameError( @@ -73,11 +132,37 @@ def register( self._registry[name] = operation_cls def get(self, name: str | tuple[str, int, int]) -> any: + """ + Retrieves a registered item from the namespace. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier of the registered operation. + + Returns + ------- + any + The registered operation class or object, or None if it is not found. + """ if name not in self._registry: return None return self._registry[name] def _remove(self, name: str | tuple[str, int, int]) -> None: + """ + Removes an item from the namespace registry. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier of the operation to remove. + + Raises + ------ + KeyError + If the specified name does not exist in the namespace. + """ if name not in self._registry: raise KeyError( f"{name} does not exists in namespace '{self.name} " @@ -91,13 +176,16 @@ def __repr__(self): return str(self) def __eq__(self, other) -> bool: + """Checks equality based on the full namespace name.""" if type(other) is not NameSpace: return False return self.name == other.name def __hash__(self) -> int: + """Hashes the namespace based on the full namespace name.""" return hash(self.name) +# NS stands for namespace, std for Standard NS_STD = NameSpace("std") # DEFAULT NAMESPACE NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace