From 7087462221d9a416677c0596ab525cf8f9fa6734 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 29 Apr 2023 08:08:19 -0400 Subject: [PATCH 01/27] Use singletons for standard library unparameterized, non-controlled gates This commit adds a new class SingletonGate which is a Gate subclass that reuses a single instance by default for all instances of a particular class. This greatly reduces the memory overhead and significant improves the construction speed for making multiple instances of the same gate. The tradeoff is in the flexibility of use because it precludes having any potentially mutable state in the shared instance. This is a large change to the data model of qiskit because it previously could be assumed that any gate instance was unique and there weren't any unintended side effects from modifying it in isolation (for example doing XGate().label = 'foo' wouldn't potentially break other code). To limit the impact around this instances of SingletonGate do not allow mutation of an existing instance. This can (and likely will) cause unexpected issues as usage of the class is released. Specifically what used to be valid will now raise an exception because it is a shared instance. This is evident from the code modifications necessary to most of the Qiskit code base to enable working with instances of SingletonGates. The knock on effects of this downstream are likely significant and managing how we roll this feature out is going to be equally if not more important than the feature itself. This is why I'm not personally convinced we want to do all this commit includes in a single release. I've opened this as a pull request primarily to start the conversation on how we want to do the roll out to try and minimize and notify downstream users of the potential breakage to avoid issues. The primary issue I have is this doesn't really follow the Qiskit deprecation policy as there is no user facing notification or documentation of this pending change and code that worked in the previously release will not work in the release with this feature. For some aspects of this change (primarily the setters on gate attributes) this can easily be handled by deprecating it in planned singleton standard library gates and waiting the necessary amount of time. But the more fundamental data model changes are hard to announce ahead of time. We can have a release note about it coming in the future but it will end up being very abstract and users will not necessarily be able to act on it ahead of time without concrete examples to test with. This was an issue for me in developing this feature as I couldn't anticipate where API breakages would occur until I switched over all the standard library gates, and there still might be others. Due to the large performance gains this offers and also in the interest of testing the API implications of using singleton gates the unparameterized and non-controlled gates available in qiskit.circuit.library.standard_gates are all updated to be subclasses of singleton gates. In aggregate this is causing construction to be roughly 6x faster and building circuits comprised solely of these gates consume 1/4th the memory as before. But it also exposed a large number of internal changes we needed to make to the wider circuit, QPY, qasm2, dagcircuit, transpiler, converters, and test modules to support working with singleton gates. Besides this there are a couple seemingly unrelated API changes in this PR and it is caused by inconsistencies in the Instruction/Gate API that were preventing this from working. The first which is the ECRGate class was missing a label kwarg in the parent. Similarly all Gate classes and subclasses were missing duration and unit kwargs on their constructors. These are necessary to be able to use these fields on singletons because we need an interface to construct an instance that has the state set so we avoid the use of the global shared instance. In the release notes I labeled these as bugfixes, because in all the cases the parent clases were exposing these interfaces and it primarily is an oversight that they were missing in these places. But personally this does seem more like something we'd normally document as a feature rather than a bugfix. A follow up PR will add a SingletonControlledGate class which will be similar to SingletonGate but will offer two singleton instance based on the value of ctrl_state (and also handle nested labels and other nested mutable state in the base gate). We can then update the standard library gates like CXGate, and CHGate to also be singletons. The ctrl state attribute is primarily why these gates were not included in this commit. --- qiskit/circuit/__init__.py | 2 + qiskit/circuit/add_control.py | 9 +- qiskit/circuit/gate.py | 12 +- qiskit/circuit/instruction.py | 14 +- qiskit/circuit/instructionset.py | 2 +- qiskit/circuit/library/standard_gates/dcx.py | 13 +- qiskit/circuit/library/standard_gates/ecr.py | 12 +- qiskit/circuit/library/standard_gates/h.py | 30 ++- qiskit/circuit/library/standard_gates/i.py | 12 +- .../circuit/library/standard_gates/iswap.py | 12 +- qiskit/circuit/library/standard_gates/s.py | 22 +- qiskit/circuit/library/standard_gates/swap.py | 24 ++- qiskit/circuit/library/standard_gates/sx.py | 40 +++- qiskit/circuit/library/standard_gates/t.py | 22 +- qiskit/circuit/library/standard_gates/x.py | 97 +++++++-- qiskit/circuit/library/standard_gates/y.py | 30 ++- qiskit/circuit/library/standard_gates/z.py | 30 ++- qiskit/circuit/random/utils.py | 2 +- qiskit/circuit/singleton_gate.py | 140 ++++++++++++ qiskit/converters/ast_to_dag.py | 12 +- qiskit/converters/circuit_to_instruction.py | 6 +- qiskit/dagcircuit/dagcircuit.py | 42 +++- qiskit/qasm2/parse.py | 3 +- qiskit/qpy/binary_io/circuits.py | 21 +- .../reset_after_measure_simplification.py | 3 +- .../passes/scheduling/dynamical_decoupling.py | 8 +- .../padding/dynamical_decoupling.py | 9 +- .../scheduling/scheduling/base_scheduler.py | 6 +- .../passes/scheduling/time_unit_conversion.py | 34 ++- .../notes/singletons-83782de8bd062cbc.yaml | 109 ++++++++++ .../circuit/test_circuit_load_from_qpy.py | 3 +- test/python/circuit/test_gate_definitions.py | 1 + test/python/circuit/test_instructions.py | 19 +- test/python/circuit/test_singleton_gate.py | 200 ++++++++++++++++++ test/python/dagcircuit/test_dagcircuit.py | 36 ++-- 35 files changed, 860 insertions(+), 177 deletions(-) create mode 100644 qiskit/circuit/singleton_gate.py create mode 100644 releasenotes/notes/singletons-83782de8bd062cbc.yaml create mode 100644 test/python/circuit/test_singleton_gate.py diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index d319cabc418c..8bfcaa8b8f3b 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + SingletonGate Control Flow Operations ----------------------- @@ -325,6 +326,7 @@ # pylint: disable=cyclic-import from .controlledgate import ControlledGate +from .singleton_gate import SingletonGate from .instruction import Instruction from .instructionset import InstructionSet from .operation import Operation diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 9a9837673d1a..ccb3cb7f3f6b 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -17,6 +17,7 @@ from qiskit.extensions import UnitaryGate from . import ControlledGate, Gate, QuantumRegister, QuantumCircuit from ._utils import _ctrl_state_to_int +from .singleton_gate import SingletonGate def add_control( @@ -55,7 +56,13 @@ def add_control( # attempt decomposition operation._define() cgate = control(operation, num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) - cgate.base_gate.label = operation.label + if operation.label is not None: + if isinstance(cgate.base_gate, SingletonGate): + cgate.base_gate = type(cgate.base_gate)( + label=operation.label, duration=operation.duration, unit=operation.unit + ) + else: + cgate.base_gate.label = operation.label return cgate diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 3bfd40f15999..65ca210b5958 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -24,7 +24,15 @@ class Gate(Instruction): """Unitary gate.""" - def __init__(self, name: str, num_qubits: int, params: list, label: str | None = None) -> None: + def __init__( + self, + name: str, + num_qubits: int, + params: list, + label: str | None = None, + duration=None, + unit="dt", + ) -> None: """Create a new gate. Args: @@ -34,7 +42,7 @@ def __init__(self, name: str, num_qubits: int, params: list, label: str | None = label: An optional label for the gate. """ self.definition = None - super().__init__(name, num_qubits, 0, params, label=label) + super().__init__(name, num_qubits, 0, params, label=label, duration=duration, unit=unit) # Set higher priority than Numpy array and matrix classes __array_priority__ = 20 diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 6721983d3bc9..f0bf3b659dcc 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -94,16 +94,24 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt self._label = label # tuple (ClassicalRegister, int), tuple (Clbit, bool) or tuple (Clbit, int) # when the instruction has a conditional ("if") - self.condition = None + self._condition = None # list of instructions (and their contexts) that this instruction is composed of # empty definition means opaque or fundamental instruction self._definition = None - self._duration = duration self._unit = unit self.params = params # must be at last (other properties may be required for validation) + @property + def condition(self): + """The classical condition on the instruction.""" + return self._condition + + @condition.setter + def condition(self, condition): + self._condition = condition + def __eq__(self, other): """Two instructions are the same if they have the same name, same dimensions, and same params. @@ -409,7 +417,7 @@ def c_if(self, classical, val): # Casting the conditional value as Boolean when # the classical condition is on a classical bit. val = bool(val) - self.condition = (classical, val) + self._condition = (classical, val) return self def copy(self, name=None): diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 8ea32f445a7d..2b1a3b756de6 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -132,7 +132,7 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc if self._requester is not None: classical = self._requester(classical) for instruction in self._instructions: - instruction.operation.c_if(classical, val) + instruction.operation = instruction.operation.c_if(classical, val) return self # Legacy support for properties. Added in Terra 0.21 to support the internal switch in diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index b11bf9af59d6..f779298f54ae 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -13,11 +13,11 @@ """Double-CNOT gate.""" import numpy as np -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister -class DCXGate(Gate): +class DCXGate(SingletonGate): r"""Double-CNOT gate. A 2-qubit Clifford gate consisting of two back-to-back @@ -47,9 +47,14 @@ class DCXGate(Gate): \end{pmatrix} """ - def __init__(self): + def __init__(self, label=None, duration=None, unit=None, _condition=None): """Create new DCX gate.""" - super().__init__("dcx", 2, []) + if unit is None: + unit = "dt" + + super().__init__( + "dcx", 2, [], label=label, _condition=_condition, 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 25a2b86c04ed..16b6912f4f36 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -14,13 +14,13 @@ from math import sqrt import numpy as np -from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.singleton_gate import SingletonGate from .rzx import RZXGate from .x import XGate -class ECRGate(Gate): +class ECRGate(SingletonGate): r"""An echoed cross-resonance gate. This gate is maximally entangling and is equivalent to a CNOT up to @@ -80,9 +80,13 @@ class ECRGate(Gate): \end{pmatrix} """ - def __init__(self): + def __init__(self, label=None, _condition=None, duration=None, unit=None): """Create new ECR gate.""" - super().__init__("ecr", 2, []) + if unit is None: + unit = "dt" + super().__init__( + "ecr", 2, [], label=label, _condition=_condition, 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 225f8666fb8c..577c4f39fc50 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -15,13 +15,13 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .t import TGate, TdgGate from .s import SGate, SdgGate -class HGate(Gate): +class HGate(SingletonGate): r"""Single-qubit Hadamard gate. This gate is a \pi rotation about the X+Z axis, and has the effect of @@ -50,9 +50,13 @@ class HGate(Gate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new H gate.""" - super().__init__("h", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "h", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -90,8 +94,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CHGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CHGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -171,10 +174,21 @@ class CHGate(ControlledGate): dtype=complex, ) - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[int, str]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[int, str]] = None, + _base_label=None, + ): """Create new CH gate.""" super().__init__( - "ch", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=HGate() + "ch", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=HGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 40e0976f69d7..4fe15a69d41a 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -14,10 +14,10 @@ from typing import Optional import numpy -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate -class IGate(Gate): +class IGate(SingletonGate): r"""Identity gate. Identity gate corresponds to a single-qubit gate wait cycle, @@ -44,9 +44,13 @@ class IGate(Gate): └───┘ """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Identity gate.""" - super().__init__("id", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "id", 1, [], label=label, _condition=_condition, 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 dcc08c146047..f8578b1982cd 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -16,13 +16,13 @@ import numpy as np -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .xx_plus_yy import XXPlusYYGate -class iSwapGate(Gate): +class iSwapGate(SingletonGate): r"""iSWAP gate. A 2-qubit XX+YY interaction. @@ -83,9 +83,13 @@ class iSwapGate(Gate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new iSwap gate.""" - super().__init__("iswap", 2, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "iswap", 2, [], label=label, _condition=_condition, 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 0fc0566b83d8..aec0ee3621b1 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -18,12 +18,12 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.library.standard_gates.p import CPhaseGate, PhaseGate from qiskit.circuit.quantumregister import QuantumRegister -class SGate(Gate): +class SGate(SingletonGate): r"""Single qubit S gate (Z**0.5). It induces a :math:`\pi/2` phase, and is sometimes called the P gate (phase). @@ -53,9 +53,13 @@ class SGate(Gate): Equivalent to a :math:`\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new S gate.""" - super().__init__("s", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "s", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -87,7 +91,7 @@ def power(self, exponent: float): return PhaseGate(0.5 * numpy.pi * exponent) -class SdgGate(Gate): +class SdgGate(SingletonGate): r"""Single qubit S-adjoint gate (~Z**0.5). It induces a :math:`-\pi/2` phase. @@ -117,9 +121,13 @@ class SdgGate(Gate): Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Sdg gate.""" - super().__init__("sdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 9e317c8eb65f..422cb8a68a36 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -15,11 +15,11 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister -class SwapGate(Gate): +class SwapGate(SingletonGate): r"""The SWAP gate. This is a symmetric and Clifford gate. @@ -54,9 +54,13 @@ class SwapGate(Gate): |a, b\rangle \rightarrow |b, a\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SWAP gate.""" - super().__init__("swap", 2, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "swap", 2, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -98,8 +102,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CSwapGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CSwapGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -214,7 +217,12 @@ class CSwapGate(ControlledGate): ] ) - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CSWAP gate.""" super().__init__( "cswap", @@ -223,7 +231,7 @@ def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, - base_gate=SwapGate(), + base_gate=SwapGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 170bde6d9e37..5bad1905e9ea 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -16,11 +16,11 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister -class SXGate(Gate): +class SXGate(SingletonGate): r"""The single-qubit Sqrt(X) gate (:math:`\sqrt{X}`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -58,9 +58,13 @@ class SXGate(Gate): """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SX gate.""" - super().__init__("sx", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sx", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -102,8 +106,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CSXGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CSXGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -112,7 +115,7 @@ def __array__(self, dtype=None): return numpy.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=dtype) / 2 -class SXdgGate(Gate): +class SXdgGate(SingletonGate): r"""The inverse single-qubit Sqrt(X) gate. Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -141,9 +144,13 @@ class SXdgGate(Gate): """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SXdg gate.""" - super().__init__("sxdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sxdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -243,10 +250,21 @@ class CSXGate(ControlledGate): ] ) - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CSX gate.""" super().__init__( - "csx", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=SXGate() + "csx", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=SXGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 8f4895d9272f..aad7288998c0 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -17,12 +17,12 @@ import numpy -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister -class TGate(Gate): +class TGate(SingletonGate): r"""Single qubit T gate (Z**0.25). It induces a :math:`\pi/4` phase, and is sometimes called the pi/8 gate @@ -53,9 +53,13 @@ class TGate(Gate): Equivalent to a :math:`\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new T gate.""" - super().__init__("t", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "t", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -87,7 +91,7 @@ def power(self, exponent: float): return PhaseGate(0.25 * numpy.pi * exponent) -class TdgGate(Gate): +class TdgGate(SingletonGate): r"""Single qubit T-adjoint gate (~Z**0.25). It induces a :math:`-\pi/4` phase. @@ -117,9 +121,13 @@ class TdgGate(Gate): Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Tdg gate.""" - super().__init__("tdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "tdg", 1, [], label=label, _condition=_condition, 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 659b6ffb4942..931b8afc8963 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -16,7 +16,7 @@ from math import ceil, pi import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int from .h import HGate @@ -26,7 +26,7 @@ from .sx import SXGate -class XGate(Gate): +class XGate(SingletonGate): r"""The single-qubit Pauli-X gate (:math:`\sigma_x`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -72,9 +72,13 @@ class XGate(Gate): |1\rangle \rightarrow |0\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new X gate.""" - super().__init__("x", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "x", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -111,8 +115,12 @@ def control( Returns: ControlledGate: controlled version of this gate. """ - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -187,10 +195,21 @@ class CXGate(ControlledGate): `|a, b\rangle \rightarrow |a, a \oplus b\rangle` """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CX gate.""" super().__init__( - "cx", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=XGate() + "cx", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=XGate(label=_base_label), ) def _define_qasm3(self): @@ -237,8 +256,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 + 1, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 1, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -325,10 +348,21 @@ class CCXGate(ControlledGate): """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CCX gate.""" super().__init__( - "ccx", 3, [], num_ctrl_qubits=2, label=label, ctrl_state=ctrl_state, base_gate=XGate() + "ccx", + 3, + [], + num_ctrl_qubits=2, + label=label, + ctrl_state=ctrl_state, + base_gate=XGate(label=_base_label), ) def _define(self): @@ -393,8 +427,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 + 2, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 2, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -411,7 +449,7 @@ def __array__(self, dtype=None): return mat -class RCCXGate(Gate): +class RCCXGate(SingletonGate): """The simplified Toffoli gate, also referred to as Margolus gate. The simplified Toffoli gate implements the Toffoli gate up to relative phases. @@ -427,9 +465,13 @@ class RCCXGate(Gate): with the :meth:`~qiskit.circuit.QuantumCircuit.rccx` method. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create a new simplified CCX gate.""" - super().__init__("rccx", 3, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "rccx", 3, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -709,7 +751,7 @@ def __array__(self, dtype=None): return mat -class RC3XGate(Gate): +class RC3XGate(SingletonGate): """The simplified 3-controlled Toffoli gate. The simplified Toffoli gate implements the Toffoli gate up to relative phases. @@ -723,9 +765,13 @@ class RC3XGate(Gate): with the :meth:`~qiskit.circuit.QuantumCircuit.rcccx` method. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create a new RC3X gate.""" - super().__init__("rcccx", 4, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "rcccx", 4, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -921,6 +967,7 @@ def __new__( num_ctrl_qubits: Optional[int] = None, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, ): """Create a new MCX instance. @@ -932,9 +979,11 @@ def __new__( explicit: dict[int, Type[ControlledGate]] = {1: CXGate, 2: CCXGate} if num_ctrl_qubits in explicit: gate_class = explicit[num_ctrl_qubits] - gate = gate_class.__new__(gate_class, label=label, ctrl_state=ctrl_state) + gate = gate_class.__new__( + gate_class, label=label, ctrl_state=ctrl_state, _base_label=_base_label + ) # if __new__ does not return the same type as cls, init is not called - gate.__init__(label=label, ctrl_state=ctrl_state) + gate.__init__(label=label, ctrl_state=ctrl_state, _base_label=_base_label) return gate return super().__new__(cls) @@ -944,6 +993,7 @@ def __init__( label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, _name="mcx", + _base_label=None, ): """Create new MCX gate.""" num_ancilla_qubits = self.__class__.get_num_ancilla_qubits(num_ctrl_qubits) @@ -954,7 +1004,7 @@ def __init__( num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state, - base_gate=XGate(), + base_gate=XGate(label=_base_label), ) def inverse(self): @@ -1029,6 +1079,7 @@ def __new__( num_ctrl_qubits: Optional[int] = None, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, ): """Create a new MCXGrayCode instance""" # if 1 to 4 control qubits, create explicit gates diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index 871aa04c2e77..cde9433307cc 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -18,11 +18,11 @@ # pylint: disable=cyclic-import from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister -class YGate(Gate): +class YGate(SingletonGate): r"""The single-qubit Pauli-Y gate (:math:`\sigma_y`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -68,9 +68,13 @@ class YGate(Gate): |1\rangle \rightarrow -i|0\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Y gate.""" - super().__init__("y", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "y", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): # pylint: disable=cyclic-import @@ -105,8 +109,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CYGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CYGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -178,10 +181,21 @@ class CYGate(ControlledGate): _matrix1 = numpy.array([[1, 0, 0, 0], [0, 0, 0, -1j], [0, 0, 1, 0], [0, 1j, 0, 0]]) _matrix0 = numpy.array([[0, 0, -1j, 0], [0, 1, 0, 0], [1j, 0, 0, 0], [0, 0, 0, 1]]) - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CY gate.""" super().__init__( - "cy", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=YGate() + "cy", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=YGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index c724fddb38b3..f14501f2b980 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -19,13 +19,13 @@ from qiskit.circuit._utils import _compute_control_matrix from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .p import PhaseGate -class ZGate(Gate): +class ZGate(SingletonGate): r"""The single-qubit Pauli-Z gate (:math:`\sigma_z`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -71,9 +71,13 @@ class ZGate(Gate): |1\rangle \rightarrow -|1\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Z gate.""" - super().__init__("z", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "z", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): # pylint: disable=cyclic-import @@ -109,8 +113,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CZGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CZGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -160,10 +163,21 @@ class CZGate(ControlledGate): the target qubit if the control qubit is in the :math:`|1\rangle` state. """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CZ gate.""" super().__init__( - "cz", 2, [], label=label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=ZGate() + "cz", + 2, + [], + label=label, + num_ctrl_qubits=1, + ctrl_state=ctrl_state, + base_gate=ZGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 2a079b304fab..91b0b679f61a 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -192,7 +192,7 @@ def random_circuit( operation = gate(*parameters[p_start:p_end]) if is_cond: # The condition values are required to be bigints, not Numpy's fixed-width type. - operation.condition = (cr, int(condition_values[c_ptr])) + operation = operation.c_if(cr, int(condition_values[c_ptr])) c_ptr += 1 qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) else: diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py new file mode 100644 index 000000000000..a8e16ea4f53e --- /dev/null +++ b/qiskit/circuit/singleton_gate.py @@ -0,0 +1,140 @@ +# 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 metaclass. +""" +from qiskit.circuit.gate import Gate +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.exceptions import QiskitError +from qiskit.circuit.exceptions import CircuitError + + +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 gate enables using + as a base class for these types of gate classes it provides a large + advantage in the memory footprint and creation speed 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 instead of using a single shared global instance of + the same gate a new separate instance will be created. + """ + + _instance = None + + @classmethod + def __new__(cls, *_args, **kwargs): + if "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 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 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 ( + self.condition is None + and self.label is None + and self.duration is None + and self.unit == "dt" + ): + return self + else: + return type(self)( + label=self.label, _condition=self.condition, duration=self.duration, unit=self.unit + ) + + def copy(self, name=None): + if name is not None and self.condition is None and self.label is None: + raise QiskitError("A custom name can not be set on a copy of a singleton gate") + return super().copy() diff --git a/qiskit/converters/ast_to_dag.py b/qiskit/converters/ast_to_dag.py index eb012ad38336..0fb7b0ea62c7 100644 --- a/qiskit/converters/ast_to_dag.py +++ b/qiskit/converters/ast_to_dag.py @@ -234,7 +234,8 @@ def _process_cnot(self, node): maxidx = max([len(id0), len(id1)]) for idx in range(maxidx): cx_gate = std.CXGate() - cx_gate.condition = self.condition + if self.condition: + cx_gate = cx_gate.c_if(*self.condition) if len(id0) > 1 and len(id1) > 1: self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], []) elif len(id0) > 1: @@ -252,7 +253,8 @@ def _process_measure(self, node): ) for idx, idy in zip(id0, id1): meas_gate = Measure() - meas_gate.condition = self.condition + if self.condition: + meas_gate = meas_gate.c_if(*self.condition) self.dag.apply_operation_back(meas_gate, [idx], [idy]) def _process_if(self, node): @@ -341,7 +343,8 @@ def _process_node(self, node): id0 = self._process_bit_id(node.children[0]) for i, _ in enumerate(id0): reset = Reset() - reset.condition = self.condition + if self.condition: + reset = reset.c_if(*self.condition) self.dag.apply_operation_back(reset, [id0[i]], []) elif node.type == "if": @@ -398,7 +401,8 @@ def _create_dag_op(self, name, params, qargs): QiskitError: if encountering a non-basis opaque gate """ op = self._create_op(name, params) - op.condition = self.condition + if self.condition: + op = op.c_if(*self.condition) self.dag.apply_operation_back(op, qargs, []) def _create_op(self, name, params): diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 7965e3a474bc..793362281b5e 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -81,7 +81,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None params=[*parameter_dict.values()], label=label, ) - out_instruction.condition = None + out_instruction._condition = None target = circuit.assign_parameters(parameter_dict, inplace=False) @@ -114,9 +114,9 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None if condition: reg, val = condition if isinstance(reg, Clbit): - rule.operation.condition = (clbit_map[reg], val) + rule.operation = rule.operation.c_if(clbit_map[reg], val) elif reg.size == c.size: - rule.operation.condition = (c, val) + rule.operation = rule.operation.c_if(c, val) else: raise QiskitError( "Cannot convert condition in circuit with " diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index f40015d7f078..00bdb31dcad6 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -828,7 +828,11 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): op = nd.op.copy() if condition and not isinstance(op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") - op.condition = condition + if condition: + if not isinstance(op, ControlFlowOp): + op = op.c_if(*condition) + else: + op.condition = condition dag.apply_operation_back(op, m_qargs, m_cargs) else: raise DAGCircuitError("bad node type %s" % type(nd)) @@ -1251,7 +1255,11 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit "cannot propagate a condition to an element that acts on those bits" ) new_op = copy.copy(in_node.op) - new_op.condition = new_condition + if new_condition: + if not isinstance(new_op, ControlFlowOp): + new_op = new_op.c_if(*new_condition) + else: + new_op.condition = new_condition in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs) else: in_dag = input_dag @@ -1335,11 +1343,19 @@ def edge_weight_map(wire): ) elif getattr(old_node.op, "condition", None) is not None: cond_target, cond_value = old_node.op.condition - m_op = copy.copy(old_node.op) - m_op.condition = ( - self._map_classical_resource_with_import(cond_target, wire_map, creg_map), - cond_value, - ) + # Deepcopy needed here in case of singletone gate usage the condition will be sticky + # globally + m_op = copy.deepcopy(old_node.op) + if not isinstance(old_node.op, ControlFlowOp): + m_op = m_op.c_if( + self._map_classical_resource_with_import(cond_target, wire_map, creg_map), + cond_value, + ) + else: + m_op.condition = ( + self._map_classical_resource_with_import(cond_target, wire_map, creg_map), + cond_value, + ) else: m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] @@ -1392,7 +1408,11 @@ def substitute_node(self, node, op, inplace=False): node.op = op if save_condition and not isinstance(op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") - node.op.condition = save_condition + if save_condition: + if not isinstance(node.op, ControlFlowOp): + node.op = node.op.c_if(*save_condition) + else: + node.op.condition = save_condition return node new_node = copy.copy(node) @@ -1400,7 +1420,11 @@ def substitute_node(self, node, op, inplace=False): new_node.op = op if save_condition and not isinstance(new_node.op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") - new_node.op.condition = save_condition + if save_condition: + if not isinstance(op, ControlFlowOp): + new_node.op = new_node.op.c_if(*save_condition) + else: + new_node.op.condition = save_condition self._multi_graph[node._node_id] = new_node if op.name != node.op.name: self._increment_op(op) diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 6f56d31b0a02..116c6b7c9aa0 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -227,8 +227,7 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): ) elif opcode == OpCode.ConditionedGate: gate_id, parameters, op_qubits, creg, value = op.operands - gate = gates[gate_id](*parameters) - gate.condition = (qc.cregs[creg], value) + gate = gates[gate_id](*parameters).c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(gate, [qubits[q] for q in op_qubits])) elif opcode == OpCode.Measure: qubit, clbit = op.operands diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 95adba8fdbd1..a54ea41f8b8a 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -14,6 +14,7 @@ """Binary IO for circuit objects.""" +import inspect import io import json import struct @@ -261,8 +262,10 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, else: raise AttributeError("Invalid instruction type: %s" % gate_name) + if instruction.label_size <= 0: + label = None if gate_name in {"IfElseOp", "WhileLoopOp"}: - gate = gate_class(condition_tuple, *params) + gate = gate_class(condition_tuple, *params, label=label) elif version >= 5 and issubclass(gate_class, ControlledGate): if gate_name in { "MCPhaseGate", @@ -272,9 +275,9 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, "MCXRecursive", "MCXVChain", }: - gate = gate_class(*params, instruction.num_ctrl_qubits) + gate = gate_class(*params, instruction.num_ctrl_qubits, label=label) else: - gate = gate_class(*params) + gate = gate_class(*params, label=label) gate.num_ctrl_qubits = instruction.num_ctrl_qubits gate.ctrl_state = instruction.ctrl_state gate.condition = condition_tuple @@ -286,10 +289,14 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, params = [len(qargs)] elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: params = [len(qargs), len(cargs)] - gate = gate_class(*params) - gate.condition = condition_tuple - if instruction.label_size > 0: - gate.label = label + if "label" in inspect.signature(gate_class).parameters: + gate = gate_class(*params, label=label) + else: + gate = gate_class(*params) + if label is not None: + gate.label = label + if condition_tuple: + gate = gate.c_if(*condition_tuple) if circuit is None: return gate if not isinstance(gate, Instruction): diff --git a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py index 4445e878ce6b..d49485784026 100644 --- a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py +++ b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py @@ -36,8 +36,7 @@ def run(self, dag): for node in dag.op_nodes(Measure): succ = next(dag.quantum_successors(node)) if isinstance(succ, DAGOpNode) and isinstance(succ.op, Reset): - new_x = XGate() - new_x.condition = (node.cargs[0], 1) + new_x = XGate().c_if(node.cargs[0], 1) new_dag = DAGCircuit() new_dag.add_qubits(node.qargs) new_dag.add_clbits(node.cargs) diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 6de3e49b5923..987f529a2bf0 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -17,6 +17,7 @@ import numpy as np from qiskit.circuit import Gate, Delay, Reset from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer @@ -193,7 +194,12 @@ def run(self, dag): for physical_qubit in self._qubits: dd_sequence_duration = 0 for gate in self._dd_sequence: - gate.duration = self._durations.get(gate, physical_qubit) + if isinstance(gate, SingletonGate): + duration = self._durations.get(gate, physical_qubit) + gate = type(gate)(label=gate.label, duration=duration, unit=gate.unit) + else: + gate.duration = self._durations.get(gate, physical_qubit) + dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index bbb1b39bbd72..5487c3a3f133 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -20,6 +20,7 @@ from qiskit.circuit.delay import Delay from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate from qiskit.circuit.reset import Reset +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode, DAGOpNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer @@ -224,7 +225,7 @@ def _pre_runhook(self, dag: DAGCircuit): continue sequence_lengths = [] - for gate in self._dd_sequence: + for index, gate in enumerate(self._dd_sequence): try: # Check calibration. gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] @@ -245,7 +246,11 @@ def _pre_runhook(self, dag: DAGCircuit): gate_length = self._durations.get(gate, physical_index) sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. - gate.duration = gate_length + if isinstance(gate, SingletonGate): + gate = type(gate)(label=gate.label, duration=gate_length) + self._dd_sequence[index] = gate + else: + gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths def __gate_supported(self, gate: Gate, qarg: int) -> bool: diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 682fb667c90f..8b8aa26058cd 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -21,6 +21,7 @@ from qiskit.dagcircuit import DAGOpNode, DAGCircuit from qiskit.circuit import Delay, Gate from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target @@ -72,7 +73,10 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) - node.op.duration = duration + if isinstance(node.op, SingletonGate): + node.op = type(node.op)(label=node.op.label, duration=duration, unit=node.op.unit) + else: + node.op.duration = duration else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 57c9587316b7..b8fc3d275cab 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -14,6 +14,7 @@ from typing import Set from qiskit.circuit import Delay +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -96,14 +97,31 @@ def run(self, dag: DAGCircuit): # Make units consistent bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for node in dag.op_nodes(): - try: - node.op = node.op.copy() - node.op.duration = self.inst_durations.get( - node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit - ) - node.op.unit = time_unit - except TranspilerError: - pass + if isinstance(node.op, SingletonGate): + try: + duration = self.inst_durations.get( + node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit + ) + # Right now singleton gates can only be non controlled and have no parameters + # so there are only 4 mutable properties that need to be set when instanitating + # new instance with duration and singleton gates can't have any extra mutable + # attributes. In the future if the use of singleton operations expands this + # will need to be adjusted to ensuure that any mutable state is preservedd + new_op = type(node.op)(label=node.op.label, duration=duration, unit=time_unit) + if node.op.condition: + new_op = new_op.c_if(*node.op.condition) + node.op = new_op + except TranspilerError: + pass + else: + try: + node.op = node.op.copy() + node.op.duration = self.inst_durations.get( + node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit + ) + node.op.unit = time_unit + except TranspilerError: + pass self.property_set["time_unit"] = time_unit return dag diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml new file mode 100644 index 000000000000..8acc852cb6b7 --- /dev/null +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -0,0 +1,109 @@ +--- +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. + 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 + 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` + 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 + 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 constructo + will be a separate instance with the custom state instead of the globally + shared instance. + - | + The following standard library gates are now instances of + :class:`~.SingletonGate`: + + * :class:`~.DCXGate` + * :class:`~.ECRGate` + * :class:`~.HGate` + * :class:`~.IGate` + * :class:`~.iSwapGate` + * :class:`~.SGate` + * :class:`~.SdgGate` + * :class:`~.SwapGate` + * :class:`~.SXGate` + * :class:`~.SXdgGate` + * :class:`~.TGate` + * :class:`~.TdgGate` + * :class:`~.XGate` + * :class:`~.RCCXGate` + * :class:`~.RC3XGate` + * :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. +upgrade: + - | + The following standard library gates: + + * :class:`~.DCXGate` + * :class:`~.ECRGate` + * :class:`~.HGate` + * :class:`~.IGate` + * :class:`~.iSwapGate` + * :class:`~.SGate` + * :class:`~.SdgGate` + * :class:`~.SwapGate` + * :class:`~.SXGate` + * :class:`~.SXdgGate` + * :class:`~.TGate` + * :class:`~.TdgGate` + * :class:`~.XGate` + * :class:`~.RCCXGate` + * :class:`~.RC3XGate` + * :class:`~.YGate` + * :class:`~.ZGate` + + no longer are able to set :attr:`~.Gate.label`, :attr:`~.Gate.condition`, + :attr:`~.Gate.duration`, or :attr:`~.Gate.unit` after insantiating 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`. This change was necssary as part of converting + these classes to be :class:`~.SingletonGate` types which greatly reduces the + - | + For anything that interacts with :class:`~.Gate`, :class:`~.Operation`, + or :class:`~.Instruction` objects or works with these as part of a + :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` classes it is important + to note that the use of shared references for instances is much more common + 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 you're use 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). +fixes: + - | + Fixed an oversight in the :class:`~.ECRGate` that prevented setting an + :attr:`.ECRGate.label` attribute at object construction time. All other + :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 + 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/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 331c30471032..69ddf05b9af6 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -603,8 +603,7 @@ def test_custom_instruction_with_noop_definition(self): def test_standard_gate_with_label(self): """Test a standard gate with a label.""" qc = QuantumCircuit(1) - gate = XGate() - gate.label = "My special X gate" + gate = XGate(label="My special X gate") qc.append(gate, [0]) qpy_file = io.BytesIO() dump(qc, qpy_file) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index b7d5f4f06834..82ef929425c6 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -282,6 +282,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "PermutationGate", "Commuting2qBlock", "PauliEvolutionGate", + "SingletonGate", } # 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: diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 385786aa9729..017b3419f148 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=unsubscriptable-object + """Test Qiskit's Instruction class.""" import unittest.mock @@ -22,6 +24,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit import QuantumRegister, ClassicalRegister, Qubit, Clbit from qiskit.circuit.library.standard_gates.h import HGate +from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit.circuit.library.standard_gates.x import CXGate from qiskit.circuit.library.standard_gates.s import SGate from qiskit.circuit.library.standard_gates.t import TGate @@ -539,21 +542,21 @@ def test_instructionset_c_if_with_no_requester(self): arbitrary :obj:`.Clbit` and `:obj:`.ClassicalRegister` instances, but rejects integers.""" with self.subTest("accepts arbitrary register"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) instructions.c_if(register, 0) self.assertIs(instruction.condition[0], register) with self.subTest("accepts arbitrary bit"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) bit = Clbit() instructions.c_if(bit, 0) self.assertIs(instruction.condition[0], bit) with self.subTest("rejects index"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) with self.assertRaisesRegex(CircuitError, r"Cannot pass an index as a condition .*"): @@ -578,7 +581,7 @@ def dummy_requester(specifier): with self.subTest("calls requester with bit"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) bit = Clbit() @@ -587,7 +590,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) index = 0 @@ -596,7 +599,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) @@ -605,7 +608,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_register) with self.subTest("calls requester only once when broadcast"): dummy_requester.reset_mock() - instruction_list = [HGate(), HGate(), HGate()] + instruction_list = [RZGate(0), RZGate(0), RZGate(0)] instructions = InstructionSet(resource_requester=dummy_requester) for instruction in instruction_list: instructions.add(instruction, [Qubit()], []) @@ -625,7 +628,7 @@ def test_label_type_enforcement(self): Instruction("h", 1, 0, [], label=0) with self.subTest("raises when a non-string label is provided to setter"): with self.assertRaisesRegex(TypeError, r"label expects a string or None"): - instruction = HGate() + instruction = RZGate(0) instruction.label = 0 diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py new file mode 100644 index 000000000000..67f92cb707dd --- /dev/null +++ b/test/python/circuit/test_singleton_gate.py @@ -0,0 +1,200 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# pylint: disable=missing-function-docstring + + +""" +Tests for singleton gate behavior +""" + +import copy + +from qiskit.circuit.library import HGate +from qiskit.circuit import Clbit, QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.converters import dag_to_circuit, circuit_to_dag + +from qiskit.test.base import QiskitTestCase + + +class TestSingletonGate(QiskitTestCase): + """Qiskit SingletonGate tests.""" + + def test_default_singleton(self): + gate = HGate() + new_gate = HGate() + self.assertIs(gate, new_gate) + + def test_label_not_singleton(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + + def test_condition_not_singleton(self): + gate = HGate() + condition_gate = HGate().c_if(Clbit(), 0) + self.assertIsNot(gate, condition_gate) + + def test_raise_on_state_mutation(self): + gate = HGate() + with self.assertRaises(NotImplementedError): + gate.label = "foo" + with self.assertRaises(NotImplementedError): + gate.condition = (Clbit(), 0) + + def test_labeled_condition(self): + singleton_gate = HGate() + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + self.assertIsNot(singleton_gate, gate) + self.assertEqual(gate.label, "conditionally special") + self.assertEqual(gate.condition, (clbit, 0)) + + def test_default_singleton_copy(self): + gate = HGate() + copied = gate.copy() + self.assertIs(gate, copied) + + def test_label_copy(self): + gate = HGate(label="special") + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_label_copy_new(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + self.assertNotEqual(gate.label, label_gate.label) + copied = gate.copy() + copied_label = label_gate.copy() + self.assertIs(gate, copied) + self.assertIsNot(copied, label_gate) + self.assertIsNot(copied_label, gate) + self.assertIsNot(copied_label, label_gate) + self.assertNotEqual(copied.label, label_gate.label) + self.assertEqual(copied_label, label_gate) + self.assertNotEqual(copied.label, "special") + self.assertEqual(copied_label.label, "special") + + def test_condition_copy(self): + gate = HGate().c_if(Clbit(), 0) + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_condition_label_copy(self): + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "conditionally special") + self.assertEqual(copied.condition, (clbit, 0)) + + def test_deepcopy(self): + gate = HGate() + copied = copy.deepcopy(gate) + self.assertIs(gate, copied) + + def test_deepcopy_with_label(self): + gate = HGate(label="special") + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "special") + + def test_deepcopy_with_condition(self): + gate = HGate().c_if(Clbit(), 0) + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_condition_label_deepcopy(self): + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "conditionally special") + self.assertEqual(copied.condition, (clbit, 0)) + + def test_label_deepcopy_new(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + self.assertNotEqual(gate.label, label_gate.label) + copied = copy.deepcopy(gate) + copied_label = copy.deepcopy(label_gate) + self.assertIs(gate, copied) + self.assertIsNot(copied, label_gate) + self.assertIsNot(copied_label, gate) + self.assertIsNot(copied_label, label_gate) + self.assertNotEqual(copied.label, label_gate.label) + self.assertEqual(copied_label, label_gate) + self.assertNotEqual(copied.label, "special") + self.assertEqual(copied_label.label, "special") + + def test_control_a_singleton(self): + singleton_gate = HGate() + gate = HGate(label="special") + ch = gate.control(label="my_ch") + self.assertEqual(ch.base_gate.label, "special") + self.assertIsNot(ch.base_gate, singleton_gate) + + def test_round_trip_dag_conversion(self): + qc = QuantumCircuit(1) + gate = HGate() + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIs(qc.data[0].operation, out.data[0].operation) + + def test_round_trip_dag_conversion_with_label(self): + gate = HGate(label="special") + qc = QuantumCircuit(1) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.label, "special") + + def test_round_trip_dag_conversion_with_condition(self): + qc = QuantumCircuit(1, 1) + gate = HGate().c_if(qc.cregs[0], 0) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + + def test_round_trip_dag_conversion_condition_label(self): + qc = QuantumCircuit(1, 1) + gate = HGate(label="conditionally special").c_if(qc.cregs[0], 0) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + self.assertEqual(out.data[0].operation.label, "conditionally special") + + def test_condition_via_instructionset(self): + gate = HGate() + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.h(qr[0]).c_if(cr, 1) + self.assertIsNot(gate, circuit.data[0].operation) + self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 48ec6b7af29f..f84cfb5256a5 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -420,8 +420,7 @@ def setUp(self): def test_apply_operation_back(self): """The apply_operation_back() method.""" - x_gate = XGate() - x_gate.condition = self.condition + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) @@ -433,8 +432,7 @@ def test_apply_operation_back(self): def test_edges(self): """Test that DAGCircuit.edges() behaves as expected with ops.""" - x_gate = XGate() - x_gate.condition = self.condition + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) @@ -452,8 +450,7 @@ def test_apply_operation_back_conditional(self): # Single qubit gate conditional: qc.h(qr[2]).c_if(cr, 3) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) h_node = self.dag.apply_operation_back(h_gate, [self.qubit2], []) self.assertEqual(h_node.qargs, (self.qubit2,)) @@ -493,8 +490,7 @@ def test_apply_operation_back_conditional_measure(self): new_creg = ClassicalRegister(1, "cr2") self.dag.add_creg(new_creg) - meas_gate = Measure() - meas_gate.condition = (new_creg, 0) + meas_gate = Measure().c_if(new_creg, 0) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit0], [self.clbit0]) self.assertEqual(meas_node.qargs, (self.qubit0,)) @@ -539,8 +535,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): # Measure targeting a clbit which _is_ a member of the conditional # register. qc.measure(qr[0], cr[0]).c_if(cr, 3) - meas_gate = Measure() - meas_gate.condition = self.condition + meas_gate = Measure().c_if(*self.condition) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit1], [self.clbit1]) self.assertEqual(meas_node.qargs, (self.qubit1,)) @@ -967,8 +962,7 @@ def test_dag_collect_runs(self): def test_dag_collect_runs_start_with_conditional(self): """Test collect runs with a conditional at the start of the run.""" - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -981,8 +975,7 @@ def test_dag_collect_runs_start_with_conditional(self): def test_dag_collect_runs_conditional_in_middle(self): """Test collect_runs with a conditional in the middle of a run.""" - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1024,8 +1017,7 @@ def test_dag_collect_1q_runs_start_with_conditional(self): """Test collect 1q runs with a conditional at the start of the run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1040,8 +1032,7 @@ def test_dag_collect_1q_runs_conditional_in_middle(self): """Test collect_1q_runs with a conditional in the middle of a run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1119,8 +1110,7 @@ def test_layers_basic(self): qubit1 = qreg[1] clbit0 = creg[0] clbit1 = creg[1] - x_gate = XGate() - x_gate.condition = (creg, 3) + x_gate = XGate().c_if(creg, 3) dag = DAGCircuit() dag.add_qreg(qreg) dag.add_creg(creg) @@ -1686,10 +1676,8 @@ def test_substitute_with_provided_wire_map_propagate_condition(self): sub.cx(0, 1) sub.h(0) - conditioned_h = HGate() - conditioned_h.condition = conditioned_cz.condition - conditioned_cx = CXGate() - conditioned_cx.condition = conditioned_cz.condition + conditioned_h = HGate().c_if(*conditioned_cz.condition) + conditioned_cx = CXGate().c_if(*conditioned_cz.condition) expected = DAGCircuit() expected.add_qubits(base_qubits) From 330beb8cea4fe06dd813961dcca3b2f7cee424a0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Jun 2023 13:53:19 -0400 Subject: [PATCH 02/27] Fix Python 3.8 compatibility There are some differences in how the inspect stdlib module behaves between python 3.8 and newer versions of python. This was causing divergence in the test and qpy behavior where inspect was used to determine different aspects of a gate (either whether label was supported as an arg or find the number of free parameters). This commit fixes this by adjusting the code to handle both newer versions of inspect as well as older ones. --- qiskit/qpy/binary_io/circuits.py | 10 ++++++++-- test/python/circuit/gate_utils.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index a54ea41f8b8a..7b0c4a70daa8 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -28,6 +28,7 @@ from qiskit.circuit import library, controlflow, CircuitInstruction from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -292,9 +293,14 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, if "label" in inspect.signature(gate_class).parameters: gate = gate_class(*params, label=label) else: - gate = gate_class(*params) if label is not None: - gate.label = label + if issubclass(gate_class, SingletonGate): + gate = gate_class(*params, label=label) + else: + gate = gate_class(*params) + gate.label = label + else: + gate = gate_class(*params) if condition_tuple: gate = gate.c_if(*condition_tuple) if circuit is None: diff --git a/test/python/circuit/gate_utils.py b/test/python/circuit/gate_utils.py index 83e884bb724b..557059c32fb4 100644 --- a/test/python/circuit/gate_utils.py +++ b/test/python/circuit/gate_utils.py @@ -25,7 +25,7 @@ def _get_free_params(fun, ignore=None): Returns: list[str]: The name of the free parameters not listed in ``ignore``. """ - ignore = ignore or [] + ignore = ignore or ["kwargs"] free_params = [] for name, param in signature(fun).parameters.items(): if param.default == Parameter.empty and param.kind != Parameter.VAR_POSITIONAL: From c653304f358a430f4aabfb45ad3231abe6640397 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Jun 2023 17:27:37 -0400 Subject: [PATCH 03/27] Simplify qpy deserialization label handling --- qiskit/qpy/binary_io/circuits.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 7b0c4a70daa8..077b1cec9393 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -290,17 +290,14 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, params = [len(qargs)] elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: params = [len(qargs), len(cargs)] - if "label" in inspect.signature(gate_class).parameters: - gate = gate_class(*params, label=label) - else: - if label is not None: - if issubclass(gate_class, SingletonGate): - gate = gate_class(*params, label=label) - else: - gate = gate_class(*params) - gate.label = label + if label is not None: + if issubclass(gate_class, SingletonGate): + gate = gate_class(*params, label=label) else: gate = gate_class(*params) + gate.label = label + else: + gate = gate_class(*params) if condition_tuple: gate = gate.c_if(*condition_tuple) if circuit is None: From 7d54237ec5127ae26d09a996703e0d588aa4c13f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Jun 2023 17:27:55 -0400 Subject: [PATCH 04/27] Remove unused classmethod decorator --- qiskit/circuit/singleton_gate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index a8e16ea4f53e..58241aef9f38 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -40,7 +40,6 @@ class SingletonGate(Gate): _instance = None - @classmethod def __new__(cls, *_args, **kwargs): if "label" in kwargs or "_condition" in kwargs or "duration" in kwargs or "unit" in kwargs: return super().__new__(cls) From 1743262b682c6dbc6e83292617ce6609b2edc408 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Jun 2023 18:19:00 -0400 Subject: [PATCH 05/27] Fix lint --- qiskit/qpy/binary_io/circuits.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 077b1cec9393..3f31271c4b69 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -14,7 +14,6 @@ """Binary IO for circuit objects.""" -import inspect import io import json import struct From aadd8cb01fd05937d5de014db7dee98f4505fee8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Jun 2023 18:26:51 -0400 Subject: [PATCH 06/27] Fix timeline drawer on output of legacy DD pass --- qiskit/transpiler/passes/scheduling/dynamical_decoupling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 987f529a2bf0..3b2e7843bacf 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -193,10 +193,11 @@ def run(self, dag): index_sequence_duration_map = {} for physical_qubit in self._qubits: dd_sequence_duration = 0 - for gate in self._dd_sequence: + for index, gate in enumerate(self._dd_sequence): if isinstance(gate, SingletonGate): duration = self._durations.get(gate, physical_qubit) gate = type(gate)(label=gate.label, duration=duration, unit=gate.unit) + self._dd_sequence[index] = gate else: gate.duration = self._durations.get(gate, physical_qubit) From ffa051cc93f050bc5693c4bb087f6d3502971efb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 21 Jun 2023 06:23:55 -0400 Subject: [PATCH 07/27] Fix doc build --- releasenotes/notes/singletons-83782de8bd062cbc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml index 8acc852cb6b7..3670ce5df630 100644 --- a/releasenotes/notes/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -84,7 +84,7 @@ upgrade: these classes to be :class:`~.SingletonGate` types which greatly reduces the - | For anything that interacts with :class:`~.Gate`, :class:`~.Operation`, - or :class:`~.Instruction` objects or works with these as part of a + or :class:`~.circuit.Instruction` objects or works with these as part of a :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` classes it is important to note that the use of shared references for instances is much more common now. Previously, it was possible to reuse and share an instance of a From 09dbcd731d397a1dc8b3caabfab7f1e70c852179 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 21 Jun 2023 09:52:46 -0400 Subject: [PATCH 08/27] Add methods to deal with mutability of singleton gates This commit adds two methods to the SingletonGate class, mutable and to_mutable. The mutable() method is a property method that returns whether a particular instance is mutable or a shared singleton instance. The second method to_mutable() returns a mutable copy of the gate. --- qiskit/circuit/add_control.py | 7 +--- qiskit/circuit/singleton_gate.py | 26 +++++++++++++ .../passes/scheduling/dynamical_decoupling.py | 6 +-- .../padding/dynamical_decoupling.py | 5 +-- .../scheduling/scheduling/base_scheduler.py | 5 +-- .../passes/scheduling/time_unit_conversion.py | 33 +++++----------- test/python/circuit/test_singleton_gate.py | 38 +++++++++++++++++++ 7 files changed, 82 insertions(+), 38 deletions(-) diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index ccb3cb7f3f6b..ed1fc29f5bca 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -58,11 +58,8 @@ def add_control( cgate = control(operation, num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) if operation.label is not None: if isinstance(cgate.base_gate, SingletonGate): - cgate.base_gate = type(cgate.base_gate)( - label=operation.label, duration=operation.duration, unit=operation.unit - ) - else: - cgate.base_gate.label = operation.label + cgate.base_gate = cgate.base_gate.to_mutable() + cgate.base_gate.label = operation.label return cgate diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 58241aef9f38..1aa297e5c1a7 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -12,6 +12,8 @@ """ Singleton metaclass. """ +import copy + from qiskit.circuit.gate import Gate from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.exceptions import QiskitError @@ -65,6 +67,30 @@ def c_if(self, classical, val): ) return instance + @property + def mutable(self) -> bool: + """Is this instance is a mutable unique instance or not. + + If this attribute is ``False`` the gate instance is a shared singleton + and is not mutable. + """ + return self is not self._instance + + def to_mutable(self): + """Return a mutable copy of this gate. + + This method will return a new mutable copy of this gate instance. + If a singleton instance is being used this will be a new unique + instance that can be mutated. If the instance is already mutable it + will be a deepcopy of that instance. + """ + if not self.mutable: + instance = super().__new__(type(self)) + instance.__init__() + return instance + else: + return copy.deepcopy(self) + @property def label(self) -> str: return self._label diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 3b2e7843bacf..62e8d3d5bb23 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -195,11 +195,9 @@ def run(self, dag): dd_sequence_duration = 0 for index, gate in enumerate(self._dd_sequence): if isinstance(gate, SingletonGate): - duration = self._durations.get(gate, physical_qubit) - gate = type(gate)(label=gate.label, duration=duration, unit=gate.unit) + gate = gate.to_mutable() self._dd_sequence[index] = gate - else: - gate.duration = self._durations.get(gate, physical_qubit) + gate.duration = self._durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 5487c3a3f133..5dd21029ce52 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -247,10 +247,9 @@ def _pre_runhook(self, dag: DAGCircuit): sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. if isinstance(gate, SingletonGate): - gate = type(gate)(label=gate.label, duration=gate_length) + gate = gate.to_mutable() self._dd_sequence[index] = gate - else: - gate.duration = gate_length + gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths def __gate_supported(self, gate: Gate, qarg: int) -> bool: diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 8b8aa26058cd..14966a80cd27 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -74,9 +74,8 @@ def _get_node_duration( # Note that node duration is updated (but this is analysis pass) if isinstance(node.op, SingletonGate): - node.op = type(node.op)(label=node.op.label, duration=duration, unit=node.op.unit) - else: - node.op.duration = duration + node.op = node.op.to_mutable() + node.op.duration = duration else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index b8fc3d275cab..63210cdf4284 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -97,31 +97,18 @@ def run(self, dag: DAGCircuit): # Make units consistent bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for node in dag.op_nodes(): + try: + duration = self.inst_durations.get( + node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit + ) + except TranspilerError: + continue if isinstance(node.op, SingletonGate): - try: - duration = self.inst_durations.get( - node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit - ) - # Right now singleton gates can only be non controlled and have no parameters - # so there are only 4 mutable properties that need to be set when instanitating - # new instance with duration and singleton gates can't have any extra mutable - # attributes. In the future if the use of singleton operations expands this - # will need to be adjusted to ensuure that any mutable state is preservedd - new_op = type(node.op)(label=node.op.label, duration=duration, unit=time_unit) - if node.op.condition: - new_op = new_op.c_if(*node.op.condition) - node.op = new_op - except TranspilerError: - pass + node.op = node.op.to_mutable() else: - try: - node.op = node.op.copy() - node.op.duration = self.inst_durations.get( - node.op, [bit_indices[qarg] for qarg in node.qargs], unit=time_unit - ) - node.op.unit = time_unit - except TranspilerError: - pass + node.op = node.op.copy() + node.op.duration = duration + node.op.unit = time_unit self.property_set["time_unit"] = time_unit return dag diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py index 67f92cb707dd..7d4c6942b2de 100644 --- a/test/python/circuit/test_singleton_gate.py +++ b/test/python/circuit/test_singleton_gate.py @@ -198,3 +198,41 @@ def test_condition_via_instructionset(self): circuit.h(qr[0]).c_if(cr, 1) self.assertIsNot(gate, circuit.data[0].operation) self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) + + def test_is_mutable(self): + gate = HGate() + self.assertFalse(gate.mutable) + label_gate = HGate(label="foo") + self.assertTrue(label_gate.mutable) + self.assertIsNot(gate, label_gate) + + def test_to_mutable(self): + gate = HGate() + self.assertFalse(gate.mutable) + new_gate = gate.to_mutable() + self.assertTrue(new_gate.mutable) + self.assertIsNot(gate, new_gate) + + def test_to_mutable_setter(self): + gate = HGate() + self.assertFalse(gate.mutable) + mutable_gate = gate.to_mutable() + mutable_gate.label = "foo" + mutable_gate.duration = 3 + mutable_gate.unit = "s" + clbit = Clbit() + mutable_gate.condition = (clbit, 0) + self.assertTrue(mutable_gate.mutable) + self.assertIsNot(gate, mutable_gate) + self.assertEqual(mutable_gate.label, "foo") + self.assertEqual(mutable_gate.duration, 3) + self.assertEqual(mutable_gate.unit, "s") + self.assertEqual(mutable_gate.condition, (clbit, 0)) + + def test_to_mutable_of_mutable_instance(self): + gate = HGate(label="foo") + mutable_copy = gate.to_mutable() + self.assertIsNot(gate, mutable_copy) + self.assertEqual(mutable_copy.label, gate.label) + mutable_copy.label = "not foo" + self.assertNotEqual(mutable_copy.label, gate.label) From c29887a5bfdb65462bf3800f0e6e276276216b0d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jul 2023 13:30:18 -0400 Subject: [PATCH 09/27] Disallow custom attribute on singleton instances This commit adds a __setattr__ method to the singleton gate class to ensure that custom attributes are not settable for a shared singleton instance. It prevents addign a custom attribute if the instance is in singleton mode and will raise a NotImplementedError to avoid silently sharing state in the single shared instance. --- qiskit/circuit/singleton_gate.py | 26 ++++++++++++++++++++++ test/python/circuit/test_singleton_gate.py | 11 ++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 1aa297e5c1a7..6e910b0e94a3 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -159,6 +159,32 @@ def __deepcopy__(self, _memo=None): 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 { + "definition", + "unit", + "duration", + "condition", + "label", + "_label", + "_condition", + "_duration", + "_unit", + "_definition", + "_name", + "_num_qubits", + "_num_clbits", + "_params", + "params", + }: + raise NotImplementedError( + "Setting custom attributes is not allowed on a singleton gate" + ) + super().__setattr__(name, value) + def copy(self, name=None): if name is not None and self.condition is None and self.label is None: raise QiskitError("A custom name can not be set on a copy of a singleton gate") diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py index 7d4c6942b2de..e728f5067bf6 100644 --- a/test/python/circuit/test_singleton_gate.py +++ b/test/python/circuit/test_singleton_gate.py @@ -19,7 +19,7 @@ import copy -from qiskit.circuit.library import HGate +from qiskit.circuit.library import HGate, SXGate from qiskit.circuit import Clbit, QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.converters import dag_to_circuit, circuit_to_dag @@ -236,3 +236,12 @@ def test_to_mutable_of_mutable_instance(self): self.assertEqual(mutable_copy.label, gate.label) mutable_copy.label = "not foo" self.assertNotEqual(mutable_copy.label, gate.label) + + def test_set_custom_attr(self): + gate = SXGate() + with self.assertRaises(NotImplementedError): + gate.custom_foo = 12345 + mutable_gate = gate.to_mutable() + self.assertTrue(mutable_gate.mutable) + mutable_gate.custom_foo = 12345 + self.assertEqual(12345, mutable_gate.custom_foo) From a788da48653da22421542223c3312cc0ad55d193 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 17 Jul 2023 09:43:14 -0400 Subject: [PATCH 10/27] Fix rebase error --- test/python/circuit/test_gate_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 69bee4589669..9589a68c7614 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -340,7 +340,7 @@ class TestStandardEquivalenceLibrary(QiskitTestCase): CU3Gate, XGate, CXGate, - ECRGate,ough. You might be interested (wr + ECRGate, CCXGate, YGate, CYGate, From 9b49dc370d95ad3793b10719e07b85c7f04163f5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 3 Aug 2023 09:37:15 -0400 Subject: [PATCH 11/27] Fix rebase issues --- qiskit/circuit/library/standard_gates/dcx.py | 1 - qiskit/circuit/library/standard_gates/i.py | 1 - qiskit/dagcircuit/dagcircuit.py | 14 +++++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index eb11247539d6..fed5ae3b442d 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -12,7 +12,6 @@ """Double-CNOT gate.""" -import numpy as np from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 3bef301e8ed2..b8742665f66c 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -13,7 +13,6 @@ """Identity gate.""" from typing import Optional -import numpy from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit._utils import with_gate_array diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index a1284f2a2b0d..de9611519871 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1346,19 +1346,15 @@ def edge_weight_map(wire): label=old_node.op.label, ) elif getattr(old_node.op, "condition", None) is not None: - cond_target, cond_value = old_node.op.condition # Deepcopy needed here in case of singletone gate usage the condition will be sticky # globally m_op = copy.deepcopy(old_node.op) if not isinstance(old_node.op, ControlFlowOp): - m_op = m_op.c_if( - *variable_mapper.map_condition(m_op.condition) - ) + new_condition = variable_mapper.map_condition(m_op.condition) + if new_condition is not None: + m_op = m_op.c_if(*new_condition) else: m_op.condition = variable_mapper.map_condition(m_op.condition) - self._map_classical_resource_with_import(cond_target, wire_map, creg_map), - cond_value, - ) else: m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] @@ -1432,9 +1428,9 @@ def substitute_node(self, node, op, inplace=False, propagate_condition=True): if not isinstance(op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") if not isinstance(node.op, ControlFlowOp): - op = node.op.c_if(*save_condition) + op = op.c_if(*old_condition) else: - op.condition = save_condition + op.condition = old_condition new_wires.update(condition_resources(old_condition).clbits) if new_wires != current_wires: From 96cf467d34de615ad3c1b3b7f94a09f837b17007 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:03:14 -0400 Subject: [PATCH 12/27] Fix module docstring --- qiskit/circuit/singleton_gate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 6e910b0e94a3..8065654fe067 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Singleton metaclass. +Singleton gate classes. """ import copy From e1a0ef46573dcf6e6e4c204a0145e847c076c332 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:07:31 -0400 Subject: [PATCH 13/27] Add .mutable and .to_mutable() to Instruction To unify the access patterns between SingletonGates and other instructions this commit adds a common property mutable and method to_mutable() to check if any Instruction (not just singletons) are mutable or not and to get a mutable copy. For things that don't inherit from SingletonGate these are hard coded to `True` and to return a copy as by default every Instruction is mutable, only `SingletonGate` objects are different in this regard. --- qiskit/circuit/instruction.py | 19 +++++++++++++++++++ qiskit/circuit/singleton_gate.py | 12 ------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index dbb7bf0416cf..d2fe33d8d3b4 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -103,6 +103,25 @@ 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 mutable(self) -> bool: + """Is this instance is a mutable unique instance or not. + + If this attribute is ``False`` the gate instance is a shared singleton + and is not mutable. + """ + return True + + def to_mutable(self): + """Return a mutable copy of this gate. + + This method will return a new mutable copy of this gate instance. + If a singleton instance is being used this will be a new unique + instance that can be mutated. If the instance is already mutable it + will be a deepcopy of that instance. + """ + return self.copy() + @property def condition(self): """The classical condition on the instruction.""" diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 8065654fe067..5d6bc794c4cc 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -69,21 +69,9 @@ def c_if(self, classical, val): @property def mutable(self) -> bool: - """Is this instance is a mutable unique instance or not. - - If this attribute is ``False`` the gate instance is a shared singleton - and is not mutable. - """ return self is not self._instance def to_mutable(self): - """Return a mutable copy of this gate. - - This method will return a new mutable copy of this gate instance. - If a singleton instance is being used this will be a new unique - instance that can be mutated. If the instance is already mutable it - will be a deepcopy of that instance. - """ if not self.mutable: instance = super().__new__(type(self)) instance.__init__() From 3784da401e2a3f3a3e9125cbbbbb8ad458943727 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:13:36 -0400 Subject: [PATCH 14/27] Unify handling of gates in scheduling passes Previously we were explicitly handling the SingletonGate class in the scheduling passes. But with the introduction of mutable and to_mutable() on Instruction we don't need to condition on gates being singleton or not and we can just handle them in the same manner as other instructions. This commit implements this change. --- qiskit/circuit/add_control.py | 3 +-- qiskit/transpiler/passes/scheduling/dynamical_decoupling.py | 6 ++---- .../passes/scheduling/padding/dynamical_decoupling.py | 6 ++---- .../passes/scheduling/scheduling/base_scheduler.py | 4 +--- qiskit/transpiler/passes/scheduling/time_unit_conversion.py | 6 +----- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index ed1fc29f5bca..463a4d1ffe76 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -57,8 +57,7 @@ def add_control( operation._define() cgate = control(operation, num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) if operation.label is not None: - if isinstance(cgate.base_gate, SingletonGate): - cgate.base_gate = cgate.base_gate.to_mutable() + cgate.base_gate = cgate.base_gate.to_mutable() cgate.base_gate.label = operation.label return cgate diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 9ee86a0e7ef0..bc606a0f7161 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -17,7 +17,6 @@ import numpy as np from qiskit.circuit import Gate, Delay, Reset from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate -from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer @@ -194,9 +193,8 @@ def run(self, dag): for physical_qubit in self._qubits: dd_sequence_duration = 0 for index, gate in enumerate(self._dd_sequence): - if isinstance(gate, SingletonGate): - gate = gate.to_mutable() - self._dd_sequence[index] = gate + gate = gate.to_mutable() + self._dd_sequence[index] = gate gate.duration = self._durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 5dd21029ce52..685eafaceff6 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -20,7 +20,6 @@ from qiskit.circuit.delay import Delay from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate from qiskit.circuit.reset import Reset -from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode, DAGOpNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer @@ -246,9 +245,8 @@ def _pre_runhook(self, dag: DAGCircuit): gate_length = self._durations.get(gate, physical_index) sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. - if isinstance(gate, SingletonGate): - gate = gate.to_mutable() - self._dd_sequence[index] = gate + gate = gate.to_mutable() + self._dd_sequence[index] = gate gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 544addfd7cbd..3792a149fd71 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -20,7 +20,6 @@ from qiskit.dagcircuit import DAGOpNode, DAGCircuit from qiskit.circuit import Delay, Gate from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.circuit.singleton_gate import SingletonGate from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target @@ -71,8 +70,7 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) - if isinstance(node.op, SingletonGate): - node.op = node.op.to_mutable() + node.op = node.op.to_mutable() node.op.duration = duration else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 3b81aaf3f615..c75e22f285b8 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -14,7 +14,6 @@ from typing import Set from qiskit.circuit import Delay -from qiskit.circuit.singleton_gate import SingletonGate from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -102,10 +101,7 @@ def run(self, dag: DAGCircuit): ) except TranspilerError: continue - if isinstance(node.op, SingletonGate): - node.op = node.op.to_mutable() - else: - node.op = node.op.copy() + node.op = node.op.to_mutable() node.op.duration = duration node.op.unit = time_unit From b90eebbdf30caf0efcb2ddb5763e060e456c458a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:25:10 -0400 Subject: [PATCH 15/27] Remove unnecessary deepcopy in substitute_node_wtih_dag --- qiskit/dagcircuit/dagcircuit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 72bac39cd73c..7f342b105fae 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1372,9 +1372,7 @@ def edge_weight_map(wire): label=old_node.op.label, ) elif getattr(old_node.op, "condition", None) is not None: - # Deepcopy needed here in case of singletone gate usage the condition will be sticky - # globally - m_op = copy.deepcopy(old_node.op) + m_op = old_node.op if not isinstance(old_node.op, ControlFlowOp): new_condition = variable_mapper.map_condition(m_op.condition) if new_condition is not None: From 4b242e12eb8da7dd13b851cef21b79cd111a051d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:34:57 -0400 Subject: [PATCH 16/27] Fix logic for SingletonGate.copy Co-authored-by: Jake Lishman --- qiskit/circuit/singleton_gate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 5d6bc794c4cc..adffb3773ee4 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -174,6 +174,6 @@ def __setattr__(self, name, value): super().__setattr__(name, value) def copy(self, name=None): - if name is not None and self.condition is None and self.label is None: - raise QiskitError("A custom name can not be set on a copy of a singleton gate") - return super().copy() + if not self.mutable and name is None: + return self + return super().copy(name=name) From d9ff18afd9ea559caf1d0dc54b211ed3a3a6402c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:38:02 -0400 Subject: [PATCH 17/27] Update Singleton Gate class docstring --- qiskit/circuit/singleton_gate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index adffb3773ee4..34377822eba3 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -23,17 +23,16 @@ 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 gate enables using - as a base class for these types of gate classes it provides a large - advantage in the memory footprint and creation speed of multiple gates. + 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. + :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 instead of using a single shared global instance of From 86ea2a03621595f3a3bee99dc733e174dbc6e3c9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:38:48 -0400 Subject: [PATCH 18/27] Remove unused imports --- qiskit/circuit/add_control.py | 1 - qiskit/circuit/singleton_gate.py | 1 - 2 files changed, 2 deletions(-) diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 463a4d1ffe76..46fe2de0c68a 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -17,7 +17,6 @@ from qiskit.extensions import UnitaryGate from . import ControlledGate, Gate, QuantumRegister, QuantumCircuit from ._utils import _ctrl_state_to_int -from .singleton_gate import SingletonGate def add_control( diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 34377822eba3..4c4d4dde64ea 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -16,7 +16,6 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError From 32d53956b190fcb016bdbb6f258e428a479f2bb2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Sep 2023 17:49:21 -0400 Subject: [PATCH 19/27] Update release notes --- .../notes/singletons-83782de8bd062cbc.yaml | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml index 3670ce5df630..b30f8c5a90de 100644 --- a/releasenotes/notes/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -26,7 +26,9 @@ features: :meth:`~.SingletonGate.c_if` for :attr:`~.SingletonGate.condition`) when creating a new object. When this is done the output from the constructo will be a separate instance with the custom state instead of the globally - shared instance. + shared instance. You can also use the :meth:`.SingeltonGate.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. - | The following standard library gates are now instances of :class:`~.SingletonGate`: @@ -54,6 +56,12 @@ features: 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. + - | + 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. upgrade: - | The following standard library gates: @@ -77,11 +85,14 @@ upgrade: * :class:`~.ZGate` no longer are able to set :attr:`~.Gate.label`, :attr:`~.Gate.condition`, - :attr:`~.Gate.duration`, or :attr:`~.Gate.unit` after insantiating an object + :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`. This change was necssary as part of converting + the use :meth:`~.Gate.c_if`. Alternatively 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 these classes to be :class:`~.SingletonGate` types which greatly reduces the + memory footprint of repeated instances of these gates. - | For anything that interacts with :class:`~.Gate`, :class:`~.Operation`, or :class:`~.circuit.Instruction` objects or works with these as part of a @@ -94,7 +105,9 @@ upgrade: library gates now built off of it). If you're use 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). + will persist through copy and deep copies). You can rely on the + :attr:`.Instruction.mutable` attribute to check the mutability of an object or + use :meth:`.Instruction.to_mutable` to get a mutable copy of any instruction. fixes: - | Fixed an oversight in the :class:`~.ECRGate` that prevented setting an From 75eb6e661bd3cee3fa7f2490e3e6d8123b914c6a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 12:42:47 -0400 Subject: [PATCH 20/27] Fix release note typos Co-authored-by: Jake Lishman --- releasenotes/notes/singletons-83782de8bd062cbc.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml index b30f8c5a90de..4d1212917061 100644 --- a/releasenotes/notes/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -7,7 +7,7 @@ features: 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 + For example, the best example of this is :class:`.XGate` doesn't contain any state and could leveerage :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 @@ -24,9 +24,9 @@ features: 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 constructo + 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:`.SingeltonGate.to_mutable` + 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. - | From b95ac96ca792fa063663eb2a86d8acc7a2d12d27 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 12:51:06 -0400 Subject: [PATCH 21/27] Improve setattr performance --- qiskit/circuit/singleton_gate.py | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 4c4d4dde64ea..b58a4f04af92 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -19,6 +19,25 @@ 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 @@ -149,23 +168,7 @@ def __setattr__(self, name, value): if self.mutable: super().__setattr__(name, value) else: - if name not in { - "definition", - "unit", - "duration", - "condition", - "label", - "_label", - "_condition", - "_duration", - "_unit", - "_definition", - "_name", - "_num_qubits", - "_num_clbits", - "_params", - "params", - }: + if name not in SINGLETONGATE_ATTR_SET: raise NotImplementedError( "Setting custom attributes is not allowed on a singleton gate" ) From 8deb2cb8af73f7d2888bef3c80adb41316fb8dc2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 12:51:17 -0400 Subject: [PATCH 22/27] Fix deepcopy logic --- qiskit/circuit/singleton_gate.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index b58a4f04af92..e67c063910bc 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -152,12 +152,7 @@ def unit(self, unit): self._unit = unit def __deepcopy__(self, _memo=None): - if ( - self.condition is None - and self.label is None - and self.duration is None - and self.unit == "dt" - ): + if not self.mutable: return self else: return type(self)( From 2479b122cd23264ba1294232b1f9f14d2a08f054 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 13:28:35 -0400 Subject: [PATCH 23/27] Add check for kwargs up front --- qiskit/circuit/singleton_gate.py | 42 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index e67c063910bc..38a2d68f5494 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -19,23 +19,25 @@ 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", -)) +SINGLETONGATE_ATTR_SET = frozenset( + ( + "definition", + "unit", + "duration", + "condition", + "label", + "_label", + "_condition", + "_duration", + "_unit", + "_definition", + "_name", + "_num_qubits", + "_num_clbits", + "_params", + "params", + ) +) class SingletonGate(Gate): @@ -59,8 +61,10 @@ class SingletonGate(Gate): _instance = None - def __new__(cls, *_args, **kwargs): - if "label" in kwargs or "_condition" in kwargs or "duration" in kwargs or "unit" in kwargs: + def __new__(cls, *args, **kwargs): + if 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) From 9c9bc0999f892bcf558fff52629bc89471b3f414 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 13:29:56 -0400 Subject: [PATCH 24/27] Fix docs typos --- qiskit/circuit/singleton_gate.py | 4 ++-- releasenotes/notes/singletons-83782de8bd062cbc.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 38a2d68f5494..6dd9aada30db 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -55,8 +55,8 @@ class SingletonGate(Gate): :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 instead of using a single shared global instance of - the same gate a new separate instance will be created. + 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 diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml index 4d1212917061..db6b761a392a 100644 --- a/releasenotes/notes/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -102,7 +102,7 @@ upgrade: 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 you're use of these objects is assuming + 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 From 37a30cb5f8a0f419c42e042c3945da5d12201fba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 13:33:02 -0400 Subject: [PATCH 25/27] Add comment on to_mutable __init__ call --- qiskit/circuit/singleton_gate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 6dd9aada30db..c34cd6b95e5d 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -95,6 +95,9 @@ def mutable(self) -> bool: 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: From ba04a8e6cc8cc8c63d1672986df05ba9817716c0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 18 Sep 2023 13:34:09 -0400 Subject: [PATCH 26/27] Fix lint --- qiskit/circuit/singleton_gate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index c34cd6b95e5d..4381dc886ee9 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -61,7 +61,7 @@ class SingletonGate(Gate): _instance = None - def __new__(cls, *args, **kwargs): + def __new__(cls, *_args, **kwargs): if kwargs and ( "label" in kwargs or "_condition" in kwargs or "duration" in kwargs or "unit" in kwargs ): From 7f99a09c05ff598dd79fa6c0b05e9b5e7f6c81e4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 19 Sep 2023 13:54:42 -0400 Subject: [PATCH 27/27] Handle positional initialization arguments in SingletonGate If there are any positional arguments set when initializing a new SingletonGate subclass those were not being handled correctly before. If there is a positional argument being set that would indicate at least a label is being set and indicates a mutable instance should be returned instead of the immutable singleton. This commit adds the missing check to the __new__ logic and also adds a test to verify the behavior is correct. --- qiskit/circuit/singleton_gate.py | 12 +++++++++--- test/python/circuit/test_singleton_gate.py | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py index 4381dc886ee9..3d24247e31ca 100644 --- a/qiskit/circuit/singleton_gate.py +++ b/qiskit/circuit/singleton_gate.py @@ -61,9 +61,15 @@ class SingletonGate(Gate): _instance = None - def __new__(cls, *_args, **kwargs): - if kwargs and ( - "label" in kwargs or "_condition" in kwargs or "duration" in kwargs or "unit" in kwargs + 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: diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py index e728f5067bf6..d8c80661d675 100644 --- a/test/python/circuit/test_singleton_gate.py +++ b/test/python/circuit/test_singleton_gate.py @@ -245,3 +245,9 @@ def test_set_custom_attr(self): self.assertTrue(mutable_gate.mutable) mutable_gate.custom_foo = 12345 self.assertEqual(12345, mutable_gate.custom_foo) + + def test_positional_label(self): + gate = SXGate() + label_gate = SXGate("I am a little label") + self.assertIsNot(gate, label_gate) + self.assertEqual(label_gate.label, "I am a little label")