diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index 49195b106dbc..e917b6874d57 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -12,7 +12,7 @@ """Double-CNOT gate.""" -from qiskit.circuit.singleton import SingletonGate +from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -52,6 +52,8 @@ def __init__(self, label=None, *, duration=None, unit="dt"): """Create new DCX gate.""" super().__init__("dcx", 2, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate dcx a, b { cx a, b; cx b, a; } diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 40e4b90e32c6..1b38f7ad08e7 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -16,7 +16,7 @@ from qiskit.circuit._utils import with_gate_array from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.singleton import SingletonGate +from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from .rzx import RZXGate from .x import XGate @@ -88,6 +88,8 @@ def __init__(self, label=None, *, duration=None, unit="dt"): """Create new ECR gate.""" super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate ecr a, b { rzx(pi/4) a, b; x a; rzx(-pi/4) a, b;} diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index d32ff7226e29..b738548cd444 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -14,7 +14,7 @@ from math import sqrt, pi from typing import Optional, Union import numpy -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -55,6 +55,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new H gate.""" super().__init__("h", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate h a { u2(0,pi) a; } @@ -181,6 +183,8 @@ def __init__( _base_label=_base_label, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate ch a,b { diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 136210fa2cfe..ec7009f37ff5 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -13,7 +13,7 @@ """Identity gate.""" from typing import Optional -from qiskit.circuit.singleton import SingletonGate +from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit._utils import with_gate_array @@ -49,6 +49,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Identity gate.""" super().__init__("id", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def inverse(self): """Invert this gate.""" return IGate() # self-inverse diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index b17be129ba07..7b3168ce0c57 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -16,7 +16,7 @@ import numpy as np -from qiskit.circuit.singleton import SingletonGate +from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -89,6 +89,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new iSwap gate.""" super().__init__("iswap", 2, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate iswap a,b { diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 45768ca552a8..345d1fd9acba 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -17,7 +17,7 @@ import numpy -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -61,6 +61,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new S gate.""" super().__init__("s", 1, [], label=label, duration=None, unit="dt") + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate s a { u1(pi/2) a; } @@ -124,6 +126,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Sdg gate.""" super().__init__("sdg", 1, [], label=label, duration=None, unit="dt") + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate sdg a { u1(-pi/2) a; } @@ -205,6 +209,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate cs a,b { h b; cp(pi/2) a,b; h b; } @@ -276,6 +282,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate csdg a,b { h b; cp(-pi/2) a,b; h b; } diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 48e45b944b9f..551e760c6932 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -14,7 +14,7 @@ from typing import Optional, Union import numpy -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -62,6 +62,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SWAP gate.""" super().__init__("swap", 2, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate swap a,b { cx a,b; cx b,a; cx a,b; } @@ -213,6 +215,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate cswap a,b,c diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 6299d14f3b0d..e60918afcbcb 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -14,7 +14,7 @@ from math import pi from typing import Optional, Union -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -66,6 +66,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SX gate.""" super().__init__("sx", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate sx a { rz(-pi/2) a; h a; rz(-pi/2); } @@ -145,6 +147,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SXdg gate.""" super().__init__("sxdg", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate sxdg a { rz(pi/2) a; h a; rz(pi/2); } @@ -245,6 +249,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate csx a,b { h b; cu1(pi/2) a,b; h b; } diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 7aaaa259b223..eaa912456d7c 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -17,7 +17,7 @@ import numpy -from qiskit.circuit.singleton import SingletonGate +from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -59,6 +59,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new T gate.""" super().__init__("t", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate t a { u1(pi/4) a; } @@ -120,6 +122,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Tdg gate.""" super().__init__("tdg", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate tdg a { u1(pi/4) a; } diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 6817203cd81f..bbee60446ce2 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -17,7 +17,7 @@ import numpy from qiskit.utils.deprecation import deprecate_func from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array @@ -75,6 +75,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new X gate.""" super().__init__("x", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate x a { u3(pi,0,pi) a; } @@ -210,6 +212,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def control( self, num_ctrl_qubits: int = 1, @@ -333,6 +337,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=2) + def _define(self): """ gate ccx a,b,c @@ -442,6 +448,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create a new simplified CCX gate.""" super().__init__("rccx", 3, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate rccx a,b,c @@ -519,6 +527,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=3) + def _define(self): """ gate c3sqrtx a,b,c,d @@ -628,6 +638,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=3) + # seems like open controls not hapening? def _define(self): """ @@ -720,9 +732,12 @@ def control( """ ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) new_ctrl_state = (self.ctrl_state << num_ctrl_qubits) | ctrl_state - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits + 3, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label - return gate + return MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 3, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) def inverse(self): """Invert this gate. The C4X is its own inverse.""" @@ -767,6 +782,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create a new RC3X gate.""" super().__init__("rcccx", 4, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): """ gate rc3x a,b,c,d @@ -859,6 +876,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=4) + # seems like open controls not hapening? def _define(self): """ @@ -929,9 +948,12 @@ def control( """ ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) new_ctrl_state = (self.ctrl_state << num_ctrl_qubits) | ctrl_state - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits + 4, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label - return gate + return MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 4, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) def inverse(self): """Invert this gate. The C4X is its own inverse.""" @@ -1052,11 +1074,12 @@ def control( """ if ctrl_state is None: # use __class__ so this works for derived classes - gate = self.__class__( - self.num_ctrl_qubits + num_ctrl_qubits, label=label, ctrl_state=ctrl_state + return self.__class__( + self.num_ctrl_qubits + num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + _base_label=self.label, ) - gate.base_gate.label = self.label - return gate return super().control(num_ctrl_qubits, label=label, ctrl_state=ctrl_state) diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index 2c00628b8d6d..1789fed72e10 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -16,7 +16,7 @@ from typing import Optional, Union # pylint: disable=cyclic-import -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -74,6 +74,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Y gate.""" super().__init__("y", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): # pylint: disable=cyclic-import from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -195,6 +197,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate cy a,b { sdg b; cx a,b; s b; } diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index ee92818575a7..1df875a81099 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -18,7 +18,7 @@ import numpy from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array -from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from .p import PhaseGate @@ -77,6 +77,8 @@ def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Z gate.""" super().__init__("z", 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def _define(self): # pylint: disable=cyclic-import from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -180,6 +182,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=1) + def _define(self): """ gate cz a,b { h b; cx a,b; h b; } @@ -265,6 +269,8 @@ def __init__( unit=unit, ) + _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=2) + def _define(self): """ gate ccz a,b,c { h c; ccx a,b,c; h c; } diff --git a/qiskit/circuit/singleton.py b/qiskit/circuit/singleton.py index bb962ba4a337..26cf5163bf0b 100644 --- a/qiskit/circuit/singleton.py +++ b/qiskit/circuit/singleton.py @@ -64,11 +64,13 @@ def __init__(self, label=None): :class-doc-from: class .. autoclass:: SingletonGate :class-doc-from: class +.. autoclass:: SingletonControlledGate + :class-doc-from: class When inheriting from one of these classes, the produced class will have an eagerly created singleton -instance (stored as a ``_singleton_instance`` property on its type object) that will be returned -whenever the class is constructed with its default arguments. This instance is immutable; attempts -to modify its properties will raise :exc:`TypeError`. +instance that will be returned whenever the class is constructed with arguments that have been +defined to be singletons. Typically this will be the defaults. These instances are immutable; +attempts to modify their properties will raise :exc:`TypeError`. *All* subclasses of :class:`~.circuit.Instruction` have a :attr:`~.Instruction.mutable` property. For most instructions this is ``True``, while for the singleton instances it is ``False``. One can @@ -115,17 +117,54 @@ class MyOtherInstruction(MyInstruction): assert MyOtherInstruction() is not MyInstruction() If for some reason you want to derive from :class:`SingletonInstruction`, or one of the related or -subclasses but *do not* want the singleton instance to be created, such as if you are defining a new -abstract base class, you can set the keyword argument ``create_default_singleton=False`` in the -class definition:: +subclasses but *do not* want the default singleton instance to be created, such as if you are +defining a new abstract base class, you can set the keyword argument +``create_default_singleton=False`` in the class definition:: class NotASingleton(SingletonInstruction, create_default_singleton=False): - pass + def __init__(self): + return super().__init__("my_mutable", 1, 0, []) assert NotASingleton() is not NotASingleton() -You cannot define a "singleton" subclass that does not have defaults for all its constructor -arguments; in this situation, you will need to set ``create_default_singleton=False``. +If your constructor does not have defaults for all its arguments, you must set +``create_default_singleton=False``. + +Subclasses of :class:`SingletonInstruction` and the other associated classes can control how their +constructor's arguments are interpreted, in order to help the singleton machinery return the +singleton even in the case than an optional argument is explicitly set to its default. + +.. automethod:: SingletonInstruction._singleton_lookup_key + +This is set by all Qiskit standard-library gates such that the :attr:`~Instruction.label` and +similar keyword arguments are ignored in the key calculation if they are their defaults, or a +mutable instance is returned if they are not. + +You can also specify other combinations of constructor arguments to produce singleton instances +for, using the ``additional_singletons`` argument in the class definition. This takes an iterable +of ``(args, kwargs)`` tuples, and will build singletons equivalent to ``cls(*args, **kwargs)``. You +do not need to handle the case of the default arguments with this. For example, given a class +definition:: + + class MySingleton(SingletonGate, additional_singletons=[((2,), {"label": "two"})]): + def __init__(self, n=1, label=None): + super().__init__("my", n, [], label=label) + + @staticmethod + def _singleton_lookup_key(n=1, label=None): + return (n, label) + +there will be two singleton instances instantiated. One corresponds to ``n=1`` and ``label=None``, +and the other to ``n=2`` and ``label="two"``. Whenever ``MySingleton`` is constructed with +arguments consistent with one of those two cases, the relavent singleton will be returned. For +example:: + + assert MySingleton() is MySingleton(1, label=None) + assert MySingleton(2, "two") is MySingleton(n=2, label="two") + +The case of the class being instantiated with zero arguments is handled specially to allow an +absolute fast-path for inner-loop performance (although the general machinery is not desperately +slow anyway). Implementation @@ -157,7 +196,7 @@ class NotASingleton(SingletonInstruction, create_default_singleton=False): 1. Before creating any singletons, we separately define the overrides needed to make an :class:`~.circuit.Instruction` and a :class:`.Gate` immutable. This is - ``_SingletonInstructionOverrides`` and ``_SingletonGateOverrides``. + ``_SingletonInstructionOverrides`` and the other ``_*Overrides`` classes. 2. While we are creating the ``XGate`` type object, we dynamically *also* create a subclass of it that has the immutable overrides in its method-resolution order in the correct place. These @@ -194,44 +233,125 @@ class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): the base class, but still able to call :class:`super`. It's more convenient to do this dynamically, closing over the desired class variable and using the two-argument form of :class:`super`, since the zero-argument form does magic introspection based on where its containing function was defined. + +Handling multiple singletons requires storing the initialization arguments in some form, to allow +the :meth:`~.Instruction.to_mutable` method and pickling to be defined. We do this as a lookup +dictionary on the singleton *type object*. This is logically an instance attribute, but because we +need to dynamically switch in the dynamic `_Singleton` type onto an instance of the base type, that +gets rather complex; either we have to require that the base already has an instance dictionary, or we +risk breaking the ``__slots__`` layout during the switch. Since the singletons have lifetimes that +last until garbage collection of their base class's type object, we can fake out this instance +dictionary using a type-object dictionary that maps instance pointers to the data we want to store. +An alternative would be to build a new type object for each individual singleton that closes over +(or stores) the initializer arguments, but type objects are quite heavy and the principle is largely +same anyway. """ -import operator +from __future__ import annotations + +import functools from .instruction import Instruction from .gate import Gate -from .controlledgate import ControlledGate - +from .controlledgate import ControlledGate, _ctrl_state_to_int -def _impl_new(cls, *_args, **_kwargs): - # __new__ for the singleton instances. - raise TypeError(f"cannot create '{cls.__name__}' instances") - -def _impl_init_subclass(base, overrides): +def _impl_init_subclass( + base: type[_SingletonBase], overrides: type[_SingletonInstructionOverrides] +): # __init_subclass__ for the classes that make their children singletons (e.g. `SingletonGate`) - def __init_subclass__(cls, *, create_default_singleton=True, **kwargs): - super(base, cls).__init_subclass__(**kwargs) - if not create_default_singleton: + def __init_subclass__( + instruction_class, *, create_default_singleton=True, additional_singletons=(), **kwargs + ): + super(base, instruction_class).__init_subclass__(**kwargs) + if not create_default_singleton and not additional_singletons: return - # We need to make a new type object that pokes in the overrides into the correct - # place in the method-resolution order. - singleton_class = _SingletonMeta.__new__( - _SingletonMeta, - f"_Singleton{cls.__name__}", - (cls, overrides), - # This is a dynamically generated class so it's got no module. The slot layout of the - # singleton class needs to match any layout in the base. - {"__module__": None, "__slots__": (), "__new__": _impl_new, "_base_class": cls}, - create_default_singleton=False, - ) - - # Make a mutable instance, fully instantiate all lazy properties, then freeze it. - cls._singleton_instance = cls(_force_mutable=True) - cls._singleton_instance._define() - cls._singleton_instance.__class__ = singleton_class + # If we're creating singleton instances, then the _type object_ needs a lookup mapping the + # "keys" to the pre-created singleton instances. It can't share this with subclasses. + instruction_class._singleton_static_lookup = {} + + class _Singleton(overrides, instruction_class, create_default_singleton=False): + __module__ = None + # We want this to match the slots layout (if any) of `cls` so it's safe to dynamically + # switch the type of an instance of `cls` to this. + __slots__ = () + + # Class variables mapping singleton instances (as pointers) to the arguments used to + # create them, for use by `to_mutable` and `__reduce__`. We're safe to use the `id` of + # (value of the pointer to) each object because they a) are singletons and b) have + # lifetimes tied to the type object in their `base_class`, so will not be garbage + # collected until the class no longer exists. This is effectively faking out an entry + # in an instance dictionary, but this works without affecting the slots layout, and + # doesn't require that the object has an instance dictionary. + _singleton_init_arguments = {} + + # Docstrings are all inherited, and we use more descriptive class methods to better + # distinguish the `_Singleton` class (`singleton_class`) from the instruction class + # (`instruction_class`) that it's wrapping. + # pylint: disable=missing-function-docstring,bad-classmethod-argument + + def __new__(singleton_class, *_args, **_kwargs): + raise TypeError(f"cannot create '{singleton_class.__name__}' instances") + + @property + def base_class(self): + return instruction_class + + @property + def mutable(self): + return False + + def to_mutable(self): + args, kwargs = type(self)._singleton_init_arguments[id(self)] + return self.base_class(*args, **kwargs, _force_mutable=True) + + def __setattr__(self, key, value): + raise TypeError( + f"This '{self.base_class.__name__}' object is immutable." + " You can get a mutable version by calling 'to_mutable()'." + ) + + def __copy__(self): + return self + + def __deepcopy__(self, memo=None): + return self + + def __reduce__(self): + # The principle is that the unpickle operation will first create the `base_class` + # type object just by re-importing its module so all the singletons are guaranteed + # to exist before we get to doing anything with these arguments. All we then need + # to do is pass the init arguments to the base type object and its logic will return + # the singleton object. + args, kwargs = type(self)._singleton_init_arguments[id(self)] + return (functools.partial(instruction_class, **kwargs), args) + + # This is just to let the type name offer slightly more hint to what's going on if it ever + # appears in an error message, so it says (e.g.) `_SingletonXGate`, not just `_Singleton`. + _Singleton.__name__ = _Singleton.__qualname__ = f"_Singleton{instruction_class.__name__}" + + def _create_singleton_instance(args, kwargs): + # Make a mutable instance, fully instantiate all lazy properties, then freeze it. + out = instruction_class(*args, **kwargs, _force_mutable=True) + out = overrides._prepare_singleton_instance(out) + out.__class__ = _Singleton + + _Singleton._singleton_init_arguments[id(out)] = (args, kwargs) + key = instruction_class._singleton_lookup_key(*args, **kwargs) + if key is not None: + instruction_class._singleton_static_lookup[key] = out + return out + + # This static lookup is only for singletons generated at class-description time. A separate + # lookup that manages an LRU or similar cache should be used for singletons created on + # demand. This static dictionary is separate to ensure that the class-requested singletons + # have lifetimes tied to the class object, while dynamic ones can be freed again. + if create_default_singleton: + instruction_class._singleton_default_instance = _create_singleton_instance((), {}) + for class_args, class_kwargs in additional_singletons: + _create_singleton_instance(class_args, class_kwargs) return classmethod(__init_subclass__) @@ -258,37 +378,119 @@ def __new__(mcs, name, bases, namespace, *, overrides=None, **kwargs): return cls def __call__(cls, *args, _force_mutable=False, **kwargs): - if not _force_mutable and not args and not kwargs: - # This class attribute is created by the singleton-creation base classes' - # `__init_subclass__` methods; see `_impl_init_subclass`. We can only be within this - # `__call__` after the class (e.g. `XGate`) was created, and `XGate._singleton_instance` - # is created during the Python-standard `XGate = type("XGate", bases, namespace)` step. - return cls._singleton_instance + if _force_mutable: + return super().__call__(*args, **kwargs) + if not args and not kwargs: + # This is a fast-path to handle constructions of the form `XGate()`, which is the + # idiomatic way of building gates during high-performance circuit construction. If + # there are any arguments or kwargs, we delegate to the overridable method to + # determine the cache key to use for lookup. + return cls._singleton_default_instance + if (key := cls._singleton_lookup_key(*args, **kwargs)) is not None: + try: + singleton = cls._singleton_static_lookup.get(key) + except TypeError: + # Catch the case of the returned key being unhashable; a subclass could not easily + # determine this because it's working with arbitrary user inputs. + singleton = None + if singleton is not None: + return singleton + # The logic can be extended to have an LRU cache for key requests that are absent, + # to allow things like parametric gates to have reusable singletons as well. return super().__call__(*args, **kwargs) -class _SingletonInstructionOverrides(Instruction): - """Overrides for all the mutable methods and properties of `Instruction` to make it - immutable.""" +class _SingletonBase(metaclass=_SingletonMeta): + """Base class of all the user-facing (library-author-facing) singleton classes such as + :class:`SingletonGate`. + + This defines the shared interface for those singletons.""" __slots__ = () - def c_if(self, classical, val): - return self.to_mutable().c_if(classical, val) + @staticmethod + def _singleton_lookup_key(*_args, **_kwargs): + """Given the arguments to the constructor, return a key tuple that identifies the singleton + instance to retrieve, or ``None`` if the arguments imply that a mutable object must be + created. + + For performance, as a special case, this method will not be called if the class constructor + was given zero arguments (e.g. the construction ``XGate()`` will not call this method, but + ``XGate(label=None)`` will), and the default singleton will immediately be returned. - @property - def base_class(self): - # `type(self)` will actually be the dynamic `_SingletonXGate` (e.g.) created by - # `SingletonGate.__init_subclass__` during the instantiation of `XGate`, since this class - # is never the concrete type of a class. - return type(self)._base_class + This static method can (and probably should) be overridden by subclasses. The derived + signature should match the class's ``__init__``; this method should then examine the + arguments to determine whether it requires mutability, or what the cache key (if any) should + be. - @property - def mutable(self): - return False + The function should return either ``None`` or valid ``dict`` key (i.e. hashable and + implements equality). Returning ``None`` means that the created instance must be mutable. + No further singleton-based processing will be done, and the class creation will proceed as + if there was no singleton handling. Otherwise, the returned key can be anything hashable + and no special meaning is ascribed to it. Whenever this method returns the same key, the + same singleton instance will be returned. We suggest that you use a tuple of the values of + all arguments that can be set while maintaining the singleton nature. - def to_mutable(self): - return self.base_class(_force_mutable=True) + Only keys that match the default arguments or arguments given to ``additional_singletons`` + at class-creation time will actually return singletons; other values will return a standard + mutable instance. + + .. note:: + + The singleton machinery will handle an unhashable return from this function gracefully + by returning a mutable instance. Subclasses should ensure that their key is hashable in + the happy path, but they do not need to manually verify that the user-supplied arguments + are hashable. For example, it's safe to implement this as:: + + @staticmethod + def _singleton_lookup_key(*args, **kwargs): + return None if kwargs else args + + even though a user might give some unhashable type as one of the ``args``. + """ + return None + + +class _frozenlist(list): + __slots__ = () + + def _reject_mutation(self, *args, **kwargs): + raise TypeError("'params' of singletons cannot be mutated") + + append = clear = extend = insert = pop = remove = reverse = sort = _reject_mutation + __setitem__ = __delitem__ = __iadd__ = __imul__ = _reject_mutation + + +class _SingletonInstructionOverrides(Instruction): + """Overrides for the mutable methods and properties of `Instruction` to make it immutable.""" + + __slots__ = () + + # The split between what's defined here and what's defined in the dynamic `_Singleton` class is + # slightly arbitrary, but generally these overrides are for things that are about the nature of + # the `Instruction` class itself, while `_Singleton` handles the Python data model and things + # that can't be written in terms of the `Instruction` interface (like the overrides of + # `base_class` and `to_mutable`). + + @staticmethod + def _prepare_singleton_instance(instruction: Instruction): + """Class-creation hook point. Given an instance of the type that these overrides correspond + to, this method should ensure that all lazy properties and caches that require mutation to + write to are eagerly defined. + + Subclass "overrides" classes can override this method if the user/library-author-facing + class they are providing overrides for has more lazy attributes or user-exposed state + with interior mutability.""" + instruction._define() + # We use this `list` subclass that rejects all mutation rather than a simple `tuple` because + # the `params` typing is specified as `list`. Various places in the library and beyond do + # `x.params.copy()` when they want to produce a version they own, which is good behaviour, + # and would fail if we switched to a `tuple`, which has no `copy` method. + instruction._params = _frozenlist(instruction._params) + return instruction + + def c_if(self, classical, val): + return self.to_mutable().c_if(classical, val) def copy(self, name=None): if name is None: @@ -297,25 +499,8 @@ def copy(self, name=None): out.name = name return out - def __setattr__(self, key, value): - raise TypeError( - f"This '{self.base_class.__name__}' object is immutable." - " You can get a mutable version by calling 'to_mutable()'." - ) - - def __copy__(self): - return self - - def __deepcopy__(self, _memo=None): - return self - - def __reduce__(self): - return (operator.attrgetter("_singleton_instance"), (self.base_class,)) - -class SingletonInstruction( - Instruction, metaclass=_SingletonMeta, overrides=_SingletonInstructionOverrides -): +class SingletonInstruction(Instruction, _SingletonBase, overrides=_SingletonInstructionOverrides): """A base class to use for :class:`~.circuit.Instruction` objects that by default are singleton instances. @@ -346,7 +531,7 @@ class _SingletonGateOverrides(_SingletonInstructionOverrides, Gate): __slots__ = () -class SingletonGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): +class SingletonGate(Gate, _SingletonBase, overrides=_SingletonGateOverrides): """A base class to use for :class:`.Gate` objects that by default are singleton instances. This class is very similar to :class:`SingletonInstruction`, except implies unitary @@ -368,7 +553,7 @@ class _SingletonControlledGateOverrides(_SingletonInstructionOverrides, Controll class SingletonControlledGate( ControlledGate, - metaclass=_SingletonMeta, + _SingletonBase, overrides=_SingletonControlledGateOverrides, ): """A base class to use for :class:`.ControlledGate` objects that by default are singleton instances @@ -379,3 +564,41 @@ class SingletonControlledGate( """ __slots__ = () + + +def stdlib_singleton_key(*, num_ctrl_qubits: int = 0): + """Create an implementation of the abstract method + :meth:`SingletonInstruction._singleton_lookup_key`, for standard-library instructions whose + ``__init__`` signatures match the one given here. + + .. warning:: + + This method is not safe for use in classes defined outside of Qiskit; it is not included in + the backwards compatibility guarantees. This is because we guarantee that the call + signatures of the base classes are backwards compatible in the sense that we will only + replace them (without warning) contravariantly, but if you use this method, you effectively + use the signature *invariantly*, and we cannot guarantee that. + + Args: + num_ctrl_qubits: if given, this implies that the gate is a :class:`.ControlledGate`, and + will have a fixed number of qubits that are used as the control. This is necessary to + allow ``ctrl_state`` to be given as either ``None`` or as an all-ones integer/string. + """ + + if num_ctrl_qubits: + + def key(label=None, ctrl_state=None, *, duration=None, unit="dt", _base_label=None): + if label is None and duration is None and unit == "dt" and _base_label is None: + # Normalisation; we want all types for the control state to key the same. + ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) + return (ctrl_state,) + return None + + else: + + def key(label=None, *, duration=None, unit="dt"): + if label is None and duration is None and unit == "dt": + return () + return None + + return staticmethod(key) diff --git a/qiskit/transpiler/passes/optimization/hoare_opt.py b/qiskit/transpiler/passes/optimization/hoare_opt.py index 508438a15fbb..a77f1985f696 100644 --- a/qiskit/transpiler/passes/optimization/hoare_opt.py +++ b/qiskit/transpiler/passes/optimization/hoare_opt.py @@ -214,7 +214,7 @@ def _traverse_dag(self, dag): if remove_ctrl: dag.substitute_node_with_dag(node, new_dag) gate = gate.base_gate - node.op = gate + node.op = gate.to_mutable() node.name = gate.name node.qargs = tuple((ctrlqb + trgtqb)[qi] for qi in qb_idx) _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) diff --git a/test/python/circuit/test_singleton.py b/test/python/circuit/test_singleton.py index 4366557e9b86..ffa3f6d076bb 100644 --- a/test/python/circuit/test_singleton.py +++ b/test/python/circuit/test_singleton.py @@ -20,8 +20,22 @@ import copy import io import pickle - -from qiskit.circuit.library import HGate, SXGate, CXGate, CZGate, CSwapGate, CHGate, CCXGate, XGate +import sys +import types +import unittest.mock +import uuid + +from qiskit.circuit.library import ( + HGate, + SXGate, + CXGate, + CZGate, + CSwapGate, + CHGate, + CCXGate, + XGate, + C4XGate, +) from qiskit.circuit import Clbit, QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.singleton import SingletonGate, SingletonInstruction from qiskit.converters import dag_to_circuit, circuit_to_dag @@ -326,6 +340,137 @@ class ESPMeasure(Measure): self.assertIs(base.base_class, Measure) self.assertIs(esp.base_class, ESPMeasure) + def test_singleton_with_default(self): + # Explicitly setting the label to its default. + gate = HGate(label=None) + self.assertIs(gate, HGate()) + self.assertIsNot(gate, HGate(label="label")) + + def test_additional_singletons(self): + additional_inputs = [ + ((1,), {}), + ((2,), {"label": "x"}), + ] + + class Discrete(SingletonGate, additional_singletons=additional_inputs): + def __init__(self, n=0, label=None): + super().__init__("discrete", 1, [], label=label) + self.n = n + + @staticmethod + def _singleton_lookup_key(n=0, label=None): # pylint: disable=arguments-differ + # This is an atypical usage - in Qiskit standard gates, the `label` being set + # not-None should not generate a singleton, so should return a mutable instance. + return (n, label) + + default = Discrete() + self.assertIs(default, Discrete()) + self.assertIs(default, Discrete(0, label=None)) + self.assertEqual(default.n, 0) + self.assertIsNot(default, Discrete(1)) + + one = Discrete(1) + self.assertIs(one, Discrete(1)) + self.assertIs(one, Discrete(1, label=None)) + self.assertEqual(one.n, 1) + self.assertIs(one.label, None) + + two = Discrete(2, label="x") + self.assertIs(two, Discrete(2, label="x")) + self.assertIsNot(two, Discrete(2)) + self.assertEqual(two.n, 2) + self.assertEqual(two.label, "x") + + # This doesn't match any of the defined singletons, and we're checking that it's not + # spuriously cached without us asking for it. + self.assertIsNot(Discrete(2), Discrete(2)) + + def test_additional_singletons_copy(self): + additional_inputs = [ + ((1,), {}), + ((2,), {"label": "x"}), + ] + + class Discrete(SingletonGate, additional_singletons=additional_inputs): + def __init__(self, n=0, label=None): + super().__init__("discrete", 1, [], label=label) + self.n = n + + @staticmethod + def _singleton_lookup_key(n=0, label=None): # pylint: disable=arguments-differ + return (n, label) + + default = Discrete() + one = Discrete(1) + two = Discrete(2, "x") + mutable = Discrete(3) + + self.assertIsNot(default, default.to_mutable()) + self.assertEqual(default.n, default.to_mutable().n) + self.assertIsNot(one, one.to_mutable()) + self.assertEqual(one.n, one.to_mutable().n) + self.assertIsNot(two, two.to_mutable()) + self.assertEqual(two.n, two.to_mutable().n) + self.assertIsNot(mutable, mutable.to_mutable()) + self.assertEqual(mutable.n, mutable.to_mutable().n) + + # The equality assertions in the middle are sanity checks that nothing got overwritten. + + self.assertIs(default, copy.copy(default)) + self.assertEqual(default.n, 0) + self.assertIs(one, copy.copy(one)) + self.assertEqual(one.n, 1) + self.assertIs(two, copy.copy(two)) + self.assertEqual(two.n, 2) + self.assertIsNot(mutable, copy.copy(mutable)) + + self.assertIs(default, copy.deepcopy(default)) + self.assertEqual(default.n, 0) + self.assertIs(one, copy.deepcopy(one)) + self.assertEqual(one.n, 1) + self.assertIs(two, copy.deepcopy(two)) + self.assertEqual(two.n, 2) + self.assertIsNot(mutable, copy.deepcopy(mutable)) + + def test_additional_singletons_pickle(self): + additional_inputs = [ + ((1,), {}), + ((2,), {"label": "x"}), + ] + + class Discrete(SingletonGate, additional_singletons=additional_inputs): + def __init__(self, n=0, label=None): + super().__init__("discrete", 1, [], label=label) + self.n = n + + @staticmethod + def _singleton_lookup_key(n=0, label=None): # pylint: disable=arguments-differ + return (n, label) + + # Pickle needs the class to be importable. We want the class to only be instantiated inside + # the test, which means we need a little magic to make it pretend-importable. + dummy_module = types.ModuleType("_QISKIT_DUMMY_" + str(uuid.uuid4()).replace("-", "_")) + dummy_module.Discrete = Discrete + Discrete.__module__ = dummy_module.__name__ + Discrete.__qualname__ = Discrete.__name__ + + default = Discrete() + one = Discrete(1) + two = Discrete(2, "x") + mutable = Discrete(3) + + with unittest.mock.patch.dict(sys.modules, {dummy_module.__name__: dummy_module}): + # The singletons in `additional_singletons` are statics; their lifetimes should be tied + # to the type object itself, so if we don't delete it, it should be eligible to be + # reloaded from and produce the exact instances. + self.assertIs(default, pickle.loads(pickle.dumps(default))) + self.assertEqual(default.n, 0) + self.assertIs(one, pickle.loads(pickle.dumps(one))) + self.assertEqual(one.n, 1) + self.assertIs(two, pickle.loads(pickle.dumps(two))) + self.assertEqual(two.n, 2) + self.assertIsNot(mutable, pickle.loads(pickle.dumps(mutable))) + class TestSingletonControlledGate(QiskitTestCase): """Qiskit SingletonGate tests.""" @@ -415,7 +560,6 @@ def test_deepcopy_with_label(self): self.assertEqual(gate, copied) self.assertEqual(copied.label, "special") self.assertTrue(copied.mutable) - self.assertIsNot(gate.base_gate, copied.base_gate) self.assertIsNot(copied, singleton_gate) self.assertEqual(singleton_gate, copied) self.assertNotEqual(singleton_gate.label, copied.label) @@ -597,3 +741,22 @@ def test_inner_outer_label_pickle(self): self.assertTrue(copied.mutable) self.assertEqual("my h gate", copied.base_gate.label) self.assertEqual("foo", copied.label) + + def test_singleton_with_defaults(self): + self.assertIs(CXGate(), CXGate(label=None)) + self.assertIs(CXGate(), CXGate(duration=None, unit="dt")) + self.assertIs(CXGate(), CXGate(_base_label=None)) + self.assertIs(CXGate(), CXGate(label=None, ctrl_state=None)) + + def test_singleton_with_equivalent_ctrl_state(self): + self.assertIs(CXGate(), CXGate(ctrl_state=None)) + self.assertIs(CXGate(), CXGate(ctrl_state=1)) + self.assertIs(CXGate(), CXGate(label=None, ctrl_state=1)) + self.assertIs(CXGate(), CXGate(ctrl_state="1")) + self.assertIsNot(CXGate(), CXGate(ctrl_state=0)) + self.assertIsNot(CXGate(), CXGate(ctrl_state="0")) + + self.assertIs(C4XGate(), C4XGate(ctrl_state=None)) + self.assertIs(C4XGate(), C4XGate(ctrl_state=15)) + self.assertIs(C4XGate(), C4XGate(ctrl_state="1111")) + self.assertIsNot(C4XGate(), C4XGate(ctrl_state=0))