diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index e2f7936acbcc..8d82228bc8ab 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + SingletonInstruction SingletonGate Control Flow Operations @@ -367,7 +368,7 @@ # pylint: disable=cyclic-import from .controlledgate import ControlledGate -from .singleton_gate import SingletonGate +from .singleton import SingletonInstruction, SingletonGate from .instruction import Instruction from .instructionset import InstructionSet from .operation import Operation diff --git a/qiskit/circuit/controlledgate.py b/qiskit/circuit/controlledgate.py index 8def8bbae209..02674f06b1ff 100644 --- a/qiskit/circuit/controlledgate.py +++ b/qiskit/circuit/controlledgate.py @@ -232,7 +232,10 @@ def params(self, parameters): CircuitError: If controlled gate does not define a base gate. """ if self.base_gate: - self.base_gate.params = parameters + if self.base_gate.mutable: + self.base_gate.params = parameters + elif parameters: + raise CircuitError("cannot set parameters on immutable base gate") else: raise CircuitError("Controlled gate does not define base gate for extracting params") diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 3b20e5859deb..05ec83ba4597 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -31,9 +31,11 @@ The circuit itself keeps this context. """ +from __future__ import annotations + import copy from itertools import zip_longest -from typing import List +from typing import List, Type import numpy @@ -103,6 +105,38 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt self.params = params # must be at last (other properties may be required for validation) + @property + def base_class(self) -> Type[Instruction]: + """Get the base class of this instruction. This is guaranteed to be in the inheritance tree + of ``self``. + + The "base class" of an instruction is the lowest class in its inheritance tree that the + object should be considered entirely compatible with for _all_ circuit applications. This + typically means that the subclass is defined purely to offer some sort of programmer + convenience over the base class, and the base class is the "true" class for a behavioural + perspective. In particular, you should *not* override :attr:`base_class` if you are + defining a custom version of an instruction that will be implemented differently by + hardware, such as an alternative measurement strategy, or a version of a parametrised gate + with a particular set of parameters for the purposes of distinguishing it in a + :class:`.Target` from the full parametrised gate. + + This is often exactly equivalent to ``type(obj)``, except in the case of singleton instances + of standard-library instructions. These singleton instances are special subclasses of their + base class, and this property will return that base. For example:: + + >>> isinstance(XGate(), XGate) + True + >>> type(XGate()) is XGate + False + >>> XGate().base_class is XGate + True + + In general, you should not rely on the precise class of an instruction; within a given + circuit, it is expected that :attr:`Instruction.name` should be a more suitable + discriminator in most situations. + """ + return type(self) + @property def mutable(self) -> bool: """Is this instance is a mutable unique instance or not. @@ -141,8 +175,9 @@ def __eq__(self, other): Returns: bool: are self and other equal. """ - if ( - type(self) is not type(other) + if ( # pylint: disable=too-many-boolean-expressions + not isinstance(other, Instruction) + or self.base_class is not other.base_class or self.name != other.name or self.num_qubits != other.num_qubits or self.num_clbits != other.num_clbits @@ -366,7 +401,13 @@ def reverse_ops(self): qiskit.circuit.Instruction: a new instruction with sub-instructions reversed. """ - if not self._definition: + # A single `Instruction` cannot really determine whether it is a "composite" instruction or + # not; it depends on greater context whether it needs to be decomposed. The `_definition` + # not existing is flaky; all that means is that nobody has _yet_ asked for its definition; + # for efficiency, most gates define this on-the-fly. The checks here are a very very + # approximate check for an "atomic" instruction, that are mostly just this way for + # historical consistency. + if not self._definition or not self.mutable: return self.copy() reverse_inst = self.copy(name=self.name + "_reverse") diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index fed5ae3b442d..49195b106dbc 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -48,14 +48,9 @@ class DCXGate(SingletonGate): \end{pmatrix} """ - def __init__(self, label=None, duration=None, unit=None, _condition=None): + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new DCX gate.""" - if unit is None: - unit = "dt" - - super().__init__( - "dcx", 2, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("dcx", 2, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index abff2072daa1..40e4b90e32c6 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from .rzx import RZXGate from .x import XGate @@ -84,13 +84,9 @@ class ECRGate(SingletonGate): \end{pmatrix} """ - def __init__(self, label=None, _condition=None, duration=None, unit=None): + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new ECR gate.""" - if unit is None: - unit = "dt" - super().__init__( - "ecr", 2, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 7a75b6094010..ddc60d8e54a8 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from .t import TGate, TdgGate @@ -54,13 +54,9 @@ class HGate(SingletonGate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new H gate.""" - if unit is None: - unit = "dt" - super().__init__( - "h", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("h", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index b8742665f66c..136210fa2cfe 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit._utils import with_gate_array @@ -45,13 +45,9 @@ class IGate(SingletonGate): └───┘ """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Identity gate.""" - if unit is None: - unit = "dt" - super().__init__( - "id", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("id", 1, [], label=label, duration=duration, unit=unit) def inverse(self): """Invert this gate.""" diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index 018715ef3d68..b17be129ba07 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -85,13 +85,9 @@ class iSwapGate(SingletonGate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new iSwap gate.""" - if unit is None: - unit = "dt" - super().__init__( - "iswap", 2, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("iswap", 2, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 07349eb96350..a0ab961e9178 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -18,7 +18,7 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.library.standard_gates.p import CPhaseGate, PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -59,13 +59,9 @@ class SGate(SingletonGate): Equivalent to a :math:`\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new S gate.""" - if unit is None: - unit = "dt" - super().__init__( - "s", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("s", 1, [], label=label, duration=None, unit="dt") def _define(self): """ @@ -124,13 +120,9 @@ class SdgGate(SingletonGate): Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Sdg gate.""" - if unit is None: - unit = "dt" - super().__init__( - "sdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("sdg", 1, [], label=label, duration=None, unit="dt") def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 5f4cc76a87e1..ef13d2767370 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -59,13 +59,9 @@ class SwapGate(SingletonGate): |a, b\rangle \rightarrow |b, a\rangle """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SWAP gate.""" - if unit is None: - unit = "dt" - super().__init__( - "swap", 2, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("swap", 2, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 6f144fed82a6..e6ede8c29b30 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -15,7 +15,7 @@ from math import pi from typing import Optional, Union from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -63,13 +63,9 @@ class SXGate(SingletonGate): """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SX gate.""" - if unit is None: - unit = "dt" - super().__init__( - "sx", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("sx", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ @@ -146,13 +142,9 @@ class SXdgGate(SingletonGate): """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SXdg gate.""" - if unit is None: - unit = "dt" - super().__init__( - "sxdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("sxdg", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index e81d33773798..7aaaa259b223 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -55,13 +55,9 @@ class TGate(SingletonGate): Equivalent to a :math:`\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new T gate.""" - if unit is None: - unit = "dt" - super().__init__( - "t", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("t", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ @@ -120,13 +116,9 @@ class TdgGate(SingletonGate): Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Tdg gate.""" - if unit is None: - unit = "dt" - super().__init__( - "tdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("tdg", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index cc17b8060ebe..9f80d2153e20 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_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array from .h import HGate @@ -76,13 +76,9 @@ class XGate(SingletonGate): |1\rangle \rightarrow |0\rangle """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new X gate.""" - if unit is None: - unit = "dt" - super().__init__( - "x", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("x", 1, [], label=label, duration=duration, unit=unit) def _define(self): """ @@ -434,13 +430,9 @@ class RCCXGate(SingletonGate): with the :meth:`~qiskit.circuit.QuantumCircuit.rccx` method. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create a new simplified CCX gate.""" - if unit is None: - unit = "dt" - super().__init__( - "rccx", 3, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("rccx", 3, [], label=label, duration=duration, unit=unit) def _define(self): """ @@ -731,13 +723,9 @@ class RC3XGate(SingletonGate): with the :meth:`~qiskit.circuit.QuantumCircuit.rcccx` method. """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create a new RC3X gate.""" - if unit is None: - unit = "dt" - super().__init__( - "rcccx", 4, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("rcccx", 4, [], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index b4e9509903e5..f6b07773dfee 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -17,7 +17,7 @@ # pylint: disable=cyclic-import from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -71,13 +71,9 @@ class YGate(SingletonGate): |1\rangle \rightarrow -i|0\rangle """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Y gate.""" - if unit is None: - unit = "dt" - super().__init__( - "y", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("y", 1, [], label=label, duration=duration, unit=unit) def _define(self): # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 9d8ba19d5fb1..1202e6490c2d 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -19,7 +19,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .p import PhaseGate @@ -74,13 +74,9 @@ class ZGate(SingletonGate): |1\rangle \rightarrow -|1\rangle """ - def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Z gate.""" - if unit is None: - unit = "dt" - super().__init__( - "z", 1, [], label=label, _condition=_condition, duration=duration, unit=unit - ) + super().__init__("z", 1, [], label=label, duration=duration, unit=unit) def _define(self): # pylint: disable=cyclic-import diff --git a/qiskit/circuit/singleton.py b/qiskit/circuit/singleton.py new file mode 100644 index 000000000000..2650376b4840 --- /dev/null +++ b/qiskit/circuit/singleton.py @@ -0,0 +1,250 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Definition of the base singleton apparatus.""" + +import operator + +from .instruction import Instruction +from .gate import Gate + +# Summary +# ======= +# +# The machinery in this file is for defining subclasses of `Instruction` and `Gate` that +# preferentially return a shared immutable singleton instance when instantiated. Taking the example +# of `XGate`, the final user-facing result is that: +# +# * There is a regular class called `XGate`, which derives from `Gate`. +# +# * Doing something like `XGate(label="my_gate")` produces an object whose type is exactly `XGate`, +# and all the mutability works completely as expected; all the methods resolve to exactly those +# defined by `XGate`, `Gate`, or parents. +# +# * Doing `XGate()` produces a singleton object whose type is a synthetic `_SingletonXGate` class, +# which derives `XGate` but overrides `__setattr__` to make itself immutable. The object itself +# has precisely the same instance attributes as `XGate()` would have if there was no singleton +# handling. This object will return itself under copy, deepcopy and roundtrip through pickle. +# +# The same is true for, for example, `Measure`, except that it's a subclass of `Instruction` only, +# and not `Gate`. +# +# From a library-author perspective, all that's needed to enhance a `Gate` or `Instruction` with +# this behaviour is to inherit from `SingletonGate` (`SingletonInstruction`) instead of `Gate` +# (`Instruction`), and for the `__init__` method to have defaults for all of its arguments (these +# will be the state of the singleton instance). For example: +# +# class XGate(SingletonGate): +# def __init__(self, label=None): +# super().__init__("x", 1, [], label=label) +# +# +# Implementation +# ============== +# +# There are several moving parts to tackle here. The behaviour of having `XGate()` return some +# singleton object that is an (inexact) instance of `XGate` but _without_ calling `__init__` +# requires us to override `type.__call__`. This means that `XGate` must have a metaclass that +# defines `__call__` to return the singleton instance. +# +# Next, we need to ensure that there _is_ a singleton instance for `XGate()` to return. This can be +# done dynamically on each call (i.e. check if the instance exists and create it if not), but since +# we also want that instance to be very special, it's easier to hook in and create it during the +# definition of the `XGate` type object. This also has the advantage that we do not need to make +# the singleton object pickleable; we only need to specify where to retrieve it from during the +# unpickle, because the creation of the base type object will recreate the singleton. +# +# We want the singleton instance to: +# +# * be immutable; it should reject all attempts to mutate itself. +# * have exactly the same state as an `XGate()` would have had if there was no singleton handling. +# +# We do this in a three-step procedure: +# +# 1. Before creating any singletons, we separately define the overrides needed to make an +# `Instruction` and a `Gate` immutable. This is `_SingletonInstructionOverrides` and +# `_SingletonGateOverrides`. +# +# 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 +# override the standard methods / properties that are defined on the mutable gate (we do not +# attempt to override any cases where the type object we are creating has extra inplace methods). +# +# 3. We can't instantiate this new subclass, because when it calls `XGate.__init__`, it will attempt +# to set some attributes, and these will be rejected by immutability. Instead, we first create a +# completely regular `XGate` instance, and then we dynamically change its type to the singleton +# class, freezing it. +# +# We could do this entirely within the metaclass machinery, but that would require `XGate` to be +# defined as something like +# +# class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): ... +# +# which is super inconvenient (or we'd have to have `_SingletonMeta` do a bunch of fragile +# introspection). Instead, we use the `abc.ABC`/`abc.ABCMeta` pattern of defining a concrete middle +# class (`SingletonGate` in the `XGate` case) that sets the metaclass, selects the overrides to be +# applied, and has an `__init_subclass__` that applies the singleton-subclass-creation steps above. +# The overrides are in separate classes so that _mutable_ `XGate` instances do not have them in +# their own MROs; doing this is easier to implement, but requires all the setters and checkers to +# dance around at runtime trying to validate whether mutating the instance is allowed. + + +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): + # __init_subclass__ for the classes that make their children singletons (e.g. `SingletonGate`) + + def __init_subclass__(cls, *, create_singleton=True, **kwargs): + super(base, cls).__init_subclass__(**kwargs) + if not create_singleton: + 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_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 + + return classmethod(__init_subclass__) + + +class _SingletonMeta(type(Instruction)): + # The inheritance above is to ensure metaclass compatibility with `Instruction`, though pylint + # doesn't understand that this is a subclass of `type`, so uses metaclass `self` conventions. + + # pylint: disable=bad-classmethod-argument,no-self-argument + + __slots__ = () + + def __new__(mcs, name, bases, namespace, *, overrides=None, **kwargs): + cls = super().__new__(mcs, name, bases, namespace, **kwargs) + if overrides is not None: + # The `__init_subclass__` logic is shared between `SingletonInstruction` and + # `SingletonGate`, but we can't do `__init_subclass__ = _impl_init_subclass(...)` + # inside the class definition bodies because it couldn't make `super()` calls. + cls.__init_subclass__ = _impl_init_subclass(cls, overrides) + 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`. + return cls._singleton_instance + return super().__call__(*args, **kwargs) + + +class _SingletonInstructionOverrides(Instruction): + """Overrides for all the mutable methods and properties of `Instruction` to make it + immutable.""" + + __slots__ = () + + def c_if(self, classical, val): + return self.to_mutable().c_if(classical, val) + + @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 + + @property + def mutable(self): + return False + + def to_mutable(self): + return self.base_class(_force_mutable=True) + + def copy(self, name=None): + if name is None: + return self + out = self.to_mutable() + out.name = name + return out + + def __setattr__(self, key, value): + raise NotImplementedError( + 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 +): + """A base class to use for :class:`~.circuit.Instruction` objects that by default are singleton + instances. + + This class should be used for instruction classes that have fixed definitions and do not contain + any unique state. The canonical example of something like this is :class:`.Measure` which has an + immutable definition and any instance of :class:`.Measure` is the same. Using singleton + instructions as a base class for these types of gate classes provides a large advantage in the + memory footprint of multiple instructions. + + The exception to be aware of with this class though are the :class:`~.circuit.Instruction` + attributes :attr:`.label`, :attr:`.condition`, :attr:`.duration`, and :attr:`.unit` which can be + set differently for specific instances of gates. For :class:`~.SingletonGate` usage to be sound + setting these attributes is not available and they can only be set at creation time, or on an + object that has been specifically made mutable using :meth:`to_mutable`. If any of these + attributes are used during creation, then instead of using a single shared global instance of + the same gate a new separate instance will be created.""" + + __slots__ = () + + +class _SingletonGateOverrides(_SingletonInstructionOverrides, Gate): + """Overrides for all the mutable methods and properties of `Gate` to make it immutable. + + This class just exists for the principle; there's no additional overrides required compared + to :class:`~.circuit.Instruction`.""" + + __slots__ = () + + +class SingletonGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): + """A base class to use for :class:`.Gate` objects that by default are singleton instances. + + This class should be used for gate classes that have fixed definitions and do not contain any + unique state. The canonical example of something like this is :class:`~.HGate` which has an + immutable definition and any instance of :class:`~.HGate` is the same. Using singleton gates as + a base class for these types of gate classes provides a large advantage in the memory footprint + of multiple gates. + + This class is very similar to :class:`SingletonInstruction`, except implies unitary + :class:`.Gate` semantics as well. The same caveats around setting attributes in that class + apply here as well.""" + + __slots__ = () diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py deleted file mode 100644 index 2fafa867b7f3..000000000000 --- a/qiskit/circuit/singleton_gate.py +++ /dev/null @@ -1,193 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Singleton gate classes. -""" -import copy - -from qiskit.circuit.gate import Gate -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.exceptions import CircuitError - - -SINGLETONGATE_ATTR_SET = frozenset( - ( - "definition", - "unit", - "duration", - "condition", - "label", - "_label", - "_condition", - "_duration", - "_unit", - "_definition", - "_name", - "_num_qubits", - "_num_clbits", - "_params", - "params", - ) -) - - -class SingletonGate(Gate): - """A base class to use for Gate objects that by default are singleton instances - - This class should be used for gate classes that have fixed definitions and - do not contain any unique state. The canonical example of something like - this is :class:`~.HGate` which has an immutable definition and any - instance of :class:`~.HGate` is the same. Using singleton gates - as a base class for these types of gate classes provides a large - advantage in the memory footprint of multiple gates. - - The exception to be aware of with this class though are the :class:`~.Gate` - attributes :attr:`.label`, :attr:`.condition`, :attr:`.duration`, and - :attr:`.unit` which can be set differently for specific instances of gates. - For :class:`~.SingletonGate` usage to be sound setting these attributes - is not available and they can only be set at creation time. If any of these - attributes are used, then instead of using a single shared global instance - of the same gate a new separate instance will be created. - """ - - _instance = None - - def __new__(cls, *args, **kwargs): - if args or ( # pylint: disable=too-many-boolean-expressions - kwargs - and ( - "label" in kwargs - or "_condition" in kwargs - or "duration" in kwargs - or "unit" in kwargs - ) - ): - return super().__new__(cls) - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self, *args, _condition=None, **kwargs): - super().__init__(*args, **kwargs) - self._condition = _condition - - def __getnewargs_ex__(self): - if not self.mutable: - return ((), {}) - return ((self.label, self._condition, self.duration, self.unit), {}) - - def c_if(self, classical, val): - if not isinstance(classical, (ClassicalRegister, Clbit)): - raise CircuitError("c_if must be used with a classical register or classical bit") - if val < 0: - raise CircuitError("condition value should be non-negative") - if isinstance(classical, Clbit): - # Casting the conditional value as Boolean when - # the classical condition is on a classical bit. - val = bool(val) - instance = type(self)( - label=self.label, _condition=(classical, val), duration=self.duration, unit=self.unit - ) - return instance - - @property - def mutable(self) -> bool: - return self is not self._instance - - def to_mutable(self): - if not self.mutable: - instance = super().__new__(type(self)) - # Coming from a shared singleton none of the arguments to - # __init__ can be set, so this is the correct behavior for - # initializing a new mutable instance - instance.__init__() - return instance - else: - return copy.deepcopy(self) - - @property - def label(self) -> str: - return self._label - - @label.setter - def label(self, label: str): - if self is self._instance: - raise NotImplementedError( - f"This gate class {type(self)} does not support manually setting a " - "label on an instance. Instead you must set the label when instantiating a new object." - ) - self._label = label - - @property - def condition(self): - return self._condition - - @condition.setter - def condition(self, condition): - if self is self._instance: - raise NotImplementedError( - f"This gate class {type(self)} does not support manually setting a " - "condition on an instance. Instead you must set the label when instantiating a new " - "object or via the .c_if() method" - ) - self._condition = condition - - @property - def duration(self): - return self._duration - - @duration.setter - def duration(self, duration): - if self is self._instance: - raise NotImplementedError( - f"This gate class {type(self)} does not support manually setting a " - "duration on an instance. Instead you must set the duration when instantiating a " - "new object." - ) - self._duration = duration - - @property - def unit(self): - return self._unit - - @unit.setter - def unit(self, unit): - if self is self._instance: - raise NotImplementedError( - f"This gate class {type(self)} does not support manually setting a " - "unit on an instance. Instead you must set the unit when instantiating a " - "new object." - ) - self._unit = unit - - def __deepcopy__(self, _memo=None): - if not self.mutable: - return self - else: - return type(self)( - label=self.label, _condition=self.condition, duration=self.duration, unit=self.unit - ) - - def __setattr__(self, name, value): - if self.mutable: - super().__setattr__(name, value) - else: - if name not in SINGLETONGATE_ATTR_SET: - raise NotImplementedError( - "Setting custom attributes is not allowed on a singleton gate" - ) - super().__setattr__(name, value) - - def copy(self, name=None): - if not self.mutable and name is None: - return self - return super().copy(name=name) diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index c9f2c977fc28..531b70cb28a7 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -314,7 +314,7 @@ def _define_custom_operation(operation, gates_to_define): # In known-good situations we want to use a manually parametrised object as the source of the # definition, but still continue to return the given object as the call-site object. - if type(operation) in known_good_parameterized: + if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) elif hasattr(operation, "_qasm2_decomposition"): new_op = operation._qasm2_decomposition() diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 0a7c4f94b7b2..44e46f9bea80 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -249,7 +249,7 @@ def __init__(self, includelist, basis_gates=()): pass def __setitem__(self, name_str, instruction): - self._data[name_str] = type(instruction) + self._data[name_str] = instruction.base_class self._data[id(instruction)] = name_str def __getitem__(self, key): diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 2e099125363e..aa1ab84d7446 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -29,7 +29,7 @@ from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate -from qiskit.circuit.singleton_gate import SingletonGate +from qiskit.circuit.singleton import SingletonInstruction, SingletonGate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -327,7 +327,7 @@ def _read_instruction( elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: params = [len(qargs), len(cargs)] if label is not None: - if issubclass(gate_class, SingletonGate): + if issubclass(gate_class, (SingletonInstruction, SingletonGate)): gate = gate_class(*params, label=label) else: gate = gate_class(*params) @@ -563,7 +563,7 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # pylint: disable=too-many-boolean-expressions def _write_instruction(file_obj, instruction, custom_operations, index_map, use_symengine): - gate_class_name = instruction.operation.__class__.__name__ + gate_class_name = instruction.operation.base_class.__name__ custom_operations_list = [] if ( ( diff --git a/qiskit/transpiler/passes/optimization/hoare_opt.py b/qiskit/transpiler/passes/optimization/hoare_opt.py index ebc89ca66283..508438a15fbb 100644 --- a/qiskit/transpiler/passes/optimization/hoare_opt.py +++ b/qiskit/transpiler/passes/optimization/hoare_opt.py @@ -291,10 +291,10 @@ def _is_identity(self, sequence): if isinstance(gate1, ControlledGate): gate1 = gate1.base_gate - gate1 = type(gate1) + gate1 = gate1.base_class if isinstance(gate2, ControlledGate): gate2 = gate2.base_gate - gate2 = type(gate2) + gate2 = gate2.base_class # equality of gates can be determined via type and parameters, unless # the gates have no specific type, in which case definition is used diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index bf130693acad..fea459b41b9e 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -132,9 +132,9 @@ def _commute_through(blocker, run, front=True): if ( preindex is not None and isinstance(blocker, DAGOpNode) - and type(blocker.op) in commutation_table + and blocker.op.base_class in commutation_table ): - commutation_rule = commutation_table[type(blocker.op)][preindex] + commutation_rule = commutation_table[blocker.op.base_class][preindex] if commutation_rule is not None: while run_clone: diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py index 5e020fd03a30..b648cbaca944 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py +++ b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py @@ -535,6 +535,8 @@ def dummy_parameter(): t_param_exp = t_param_exp.assign(t_param, new_param) sub_node_params.append(t_param_exp) template_params.append(t_param_exp) + if not node.op.mutable: + node.op = node.op.to_mutable() node.op.params = sub_node_params for node in template_dag_dep.get_nodes(): @@ -548,6 +550,8 @@ def dummy_parameter(): ) sub_node_params.append(param_exp) + if not node.op.mutable: + node.op = node.op.to_mutable() node.op.params = sub_node_params # Create the fake binding dict and check @@ -599,6 +603,8 @@ def dummy_parameter(): param_exp = float(param_exp) bound_params.append(param_exp) + if not node.op.mutable: + node.op = node.op.to_mutable() node.op.params = bound_params return template_dag_dep diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 30a8ebfe4e30..145cebd5c366 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -152,7 +152,7 @@ def _error(circuit, target=None, qubits=None): keys = target.operation_names_for_qargs(inst_qubits) for key in keys: target_op = target.operation_from_name(key) - if isinstance(target_op, type(inst.operation)) and ( + if isinstance(target_op, inst.operation.base_class) and ( target_op.is_parameterized() or all( isclose(float(p1), float(p2)) @@ -784,7 +784,7 @@ def is_controlled(gate): error = 0.0 basis_2q_fidelity[strength] = 1 - error # rewrite XX of the same strength in terms of it - embodiment = XXEmbodiments[type(v)] + embodiment = XXEmbodiments[v.base_class] if len(embodiment.parameters) == 1: embodiments[strength] = embodiment.assign_parameters([strength]) else: @@ -804,7 +804,7 @@ def is_controlled(gate): basis_fidelity=basis_2q_fidelity, pulse_optimize=True, ) - embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]}) + embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]}) else: pi2_decomposer = None decomposer = XXDecomposer( diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index ddc65e1be250..7233f57d4f7a 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -725,7 +725,7 @@ def instruction_supported( target contains the operation). Normally you would not set this argument if you wanted to check more generally that the target supports an operation with the ``parameters`` on any qubits. - operation_class (qiskit.circuit.Instruction): The operation class to check whether + operation_class (Type[qiskit.circuit.Instruction]): The operation class to check whether the target supports a particular operation by class rather than by name. This lookup is more expensive as it needs to iterate over all operations in the target instead of just a diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index fedadaa154a0..89fb19673b63 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -987,7 +987,7 @@ def draw_wires(self, wires): def special_label(node): """Some instructions have special labels""" labels = {IGate: "I", SXGate: "√X", SXdgGate: "√Xdg"} - node_type = type(node) + node_type = node.base_class return labels.get(node_type, None) @staticmethod diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml index db6b761a392a..36f9eadc3571 100644 --- a/releasenotes/notes/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -1,34 +1,31 @@ --- features: - | - Introduced a new class :class:`~.SingletonGate` which is a subclass of - :class:`~.Gate` that uses a single instance for all objects of that type. + Introduced two new classes, :class:`.SingletonInstruction` and :class:`~.SingletonGate`, which + are subclasses of :class:`~.circuit.Instruction` and :class:`~.Gate` respectively, that use a + single instance for all objects of that type. The intent behind this class is to minimize the memory and construction overhead of using multiple gates in a circuit with the tradeoff of having global shared state. For this reason this class is only applicable to gates that do not have any unique and/or mutable state stored in an instance. For example, the best example of this is :class:`.XGate` doesn't contain - any state and could leveerage :class:`~.SingletonGate` (and does starting in + any state and could leverage :class:`~.SingletonGate` (and does starting in this release), while :class:`~.RXGate` stores an angle parameter in an instance and thus can not use :class:`~.SingletonGate` because a single shared global instance can not represent the parameter values. The other potential issue to be aware of when using this class is around the - use of the :class:`~.SingletonGate` class is that the :class:`~.Gate` + use of the singleton classes is that the :class:`~.circuit.Instruction` data model supports some mutable state. Specifically, the - :attr:`~.Gate.label`, :attr:`~.Gate.duration`, :attr:`~.Gate.unit`, and - :attr:`~.Gate.condition` attributes are all accessible and mutable in the - :class:`~.Gate` and its direct subclasses. However, this is incompatible - with having a shared object via :class:`~.SingletonGate`. For instances of - :class:`~.SingletonGate` setting these attributes directly is not allowed + :attr:`~.Instruction.label`, :attr:`~.Instruction.duration`, :attr:`~.Instruction.unit`, and + :attr:`~.Instruction.condition` attributes are all accessible and mutable in the + :class:`~.circuit.Instruction` and its direct subclasses. However, this is incompatible + with having a shared object via :class:`~.SingletonInstruction`. For instances of + :class:`~.SingletonInstruction`, setting these attributes directly is not allowed and it will raise an exception. If they are needed for a particular - instance you must set them on the constructor (or via - :meth:`~.SingletonGate.c_if` for :attr:`~.SingletonGate.condition`) when - creating a new object. When this is done the output from the constructor - will be a separate instance with the custom state instead of the globally - shared instance. You can also use the :meth:`.SingletonGate.to_mutable` - method to get a mutable copy of a gate object and then mutate the attributes - like you would on any other :class:`~.circuit.Instruction` object. + instance you must ensure you have a mutable instance using :meth:`.Instruction.to_mutable` + (or use :meth:`.Instruction.c_if` for :attr:`~.Instruction.condition`), or + ``label``, ``duration`` and ``unit`` can be given as keyword arguments during class construction. - | The following standard library gates are now instances of :class:`~.SingletonGate`: @@ -51,17 +48,23 @@ features: * :class:`~.YGate` * :class:`~.ZGate` - This means that unless a ``label``, ``condition``, ``duration``, or ``unit`` - are set on the instance at creation time they will all share a single global - instance whenever a new gate object is created. This results in large reduction - in the memory overhead for > 1 object of these types and significantly faster - object construction time. + This means that if these classes are instantiated as (e.g.) ``XGate()`` using + all the constructor defaults, they will all share a single global + instance. This results in large reduction in the memory overhead for > 1 + object of these types and significantly faster object construction time. - | Added a new method :meth`.Instruction.to_mutable` and attribute :attr:`.Instruction.mutable` which is used to get a mutable copy and check whether an :class:`~.circuit.Instruction` object is mutable. With the introduction of :class:`~.SingletonGate` these methods can be used to have a unified interface to deal with the mutablitiy of instruction objects. + - | + Added an attribute :attr:`.Instruction.base_class`, which gets the "base" type of an + instruction. Many instructions will satisfy ``type(obj) == obj.base_class``, however the + singleton instances of :class:`.SingletonInstruction` and :class:`.SingletonGate` are subclasses + of their base type. You can use the new :attr:`~.Instruction.base_class` attribute to find the + base class of these. See the attribute documentation for considerations on when other + subclasses may modify their :attr:`~.Instruction.base_class`, and what this means for execution. upgrade: - | The following standard library gates: @@ -86,11 +89,13 @@ upgrade: no longer are able to set :attr:`~.Gate.label`, :attr:`~.Gate.condition`, :attr:`~.Gate.duration`, or :attr:`~.Gate.unit` after instantiating an object - anymore. You will now only be able to set these attributes as arguments - when creating a new object or in the case of :attr:`~.Gate.condtion` through - the use :meth:`~.Gate.c_if`. Alternatively you can use :meth:`~.Gate.to_mutable` + anymore. :attr:`~.Gate.condition` can be set through + the use :meth:`~.Gate.c_if`. You can use :meth:`~.Gate.to_mutable` to get a mutable copy of the instruction and then use the setter on that copy - instead of the original object. This change was necssary as part of converting + instead of the original object. ``label``, ``duration`` and ``unit`` can + be given as keyword arguments to these gates at construction time, and a + mutable instance will be returned automatically. + This change was necssary as part of converting these classes to be :class:`~.SingletonGate` types which greatly reduces the memory footprint of repeated instances of these gates. - | @@ -101,8 +106,9 @@ upgrade: now. Previously, it was possible to reuse and share an instance of a a circuit operation it wasn't very commonly used and a copy would generate a unique instance. This has changed starting in this release because of - :class:`~.SingletonGate` being made available (and a large number of standard - library gates now built off of it). If your usage of these objects is assuming + :class:`~.SingletonInstruction` and :class:`.SingletonGate` being made available (and a large + number of standard library gates now built off of it). If your usage of these objects is + assuming unique instances for every circuit operation there are potential issue because of shared state that will be reused between operations of the same type (that will persist through copy and deep copies). You can rely on the @@ -115,8 +121,8 @@ fixes: :class:`~.Gate` classes and subclasses enable setting a ``label`` keyword argument in the constructor. - | - Fixed an oversight in the :class:`~.Gate` (and all its subclasses) constructor - where the :attr:`.Gate.duration` and :attr:`.Gate.unit` attributes could not + Fixed an oversight in the :class:`~.Gate` (and standard-library subclasses) constructor + where the :attr:`~.Gate.duration` and :attr:`~.Gate.unit` attributes could not be set as keyword arguments during construction. The parent class :class:`~.circuit.Instruction` supported setting this but :class:`~.Gate` was previously not exposing this interface correctly. diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 2defc837d66b..adf0e189db32 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -213,7 +213,7 @@ def test_dag_conversion(self): dag = circuit_to_dag(circuit) expected_ops = {"HGate", "CXGate", "PauliEvolutionGate"} - ops = {node.op.__class__.__name__ for node in dag.op_nodes()} + ops = {node.op.base_class.__name__ for node in dag.op_nodes()} self.assertEqual(ops, expected_ops) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 9589a68c7614..36498beef216 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -285,6 +285,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "SingletonGate", "_U0Gate", "_DefinedGate", + "_SingletonGateOverrides", } # Amazingly, Python's scoping rules for class bodies means that this is the closest we can get # to a "natural" comprehension or functional iterable definition: