From e62c86bbc2720883270641f1c154743a657cfca8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 19 Sep 2023 15:41:30 -0400 Subject: [PATCH] Use singletons for standard library unparameterized, non-controlled gates (#10314) * 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. * 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. * Simplify qpy deserialization label handling * Remove unused classmethod decorator * Fix lint * Fix timeline drawer on output of legacy DD pass * Fix doc build * 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. * 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. * Fix rebase error * Fix rebase issues * Fix module docstring * 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. * 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. * Remove unnecessary deepcopy in substitute_node_wtih_dag * Fix logic for SingletonGate.copy Co-authored-by: Jake Lishman * Update Singleton Gate class docstring * Remove unused imports * Update release notes * Fix release note typos Co-authored-by: Jake Lishman * Improve setattr performance * Fix deepcopy logic * Add check for kwargs up front * Fix docs typos * Add comment on to_mutable __init__ call * Fix lint * 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. --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- qiskit/circuit/__init__.py | 2 + qiskit/circuit/add_control.py | 4 +- qiskit/circuit/gate.py | 12 +- qiskit/circuit/instruction.py | 33 ++- 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 | 188 +++++++++++++ qiskit/converters/ast_to_dag.py | 12 +- qiskit/converters/circuit_to_instruction.py | 6 +- qiskit/dagcircuit/dagcircuit.py | 25 +- qiskit/qasm2/parse.py | 3 +- qiskit/qpy/binary_io/circuits.py | 28 +- .../reset_after_measure_simplification.py | 3 +- .../passes/scheduling/dynamical_decoupling.py | 5 +- .../padding/dynamical_decoupling.py | 4 +- .../scheduling/scheduling/base_scheduler.py | 1 + .../passes/scheduling/time_unit_conversion.py | 9 +- .../notes/singletons-83782de8bd062cbc.yaml | 122 +++++++++ test/python/circuit/gate_utils.py | 2 +- .../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 | 253 ++++++++++++++++++ test/python/dagcircuit/test_dagcircuit.py | 36 +-- 36 files changed, 950 insertions(+), 169 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 b293fcdd12c2..e2f7936acbcc 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + SingletonGate Control Flow Operations ----------------------- @@ -366,6 +367,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..46fe2de0c68a 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -55,7 +55,9 @@ 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: + cgate.base_gate = cgate.base_gate.to_mutable() + 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 6b74dacfaab8..d2fe33d8d3b4 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -94,16 +94,43 @@ 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 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.""" + 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 +436,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 836a81ffa4cd..fed5ae3b442d 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -12,13 +12,13 @@ """Double-CNOT gate.""" -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]]) -class DCXGate(Gate): +class DCXGate(SingletonGate): r"""Double-CNOT gate. A 2-qubit Clifford gate consisting of two back-to-back @@ -48,9 +48,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 cc7b13ecc082..abff2072daa1 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -15,8 +15,8 @@ import numpy as np from qiskit.circuit._utils import with_gate_array -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 @@ -24,7 +24,7 @@ @with_gate_array( sqrt(0.5) * np.array([[0, 1, 0, 1.0j], [1, 0, -1.0j, 0], [0, 1.0j, 0, 1], [-1.0j, 0, 1, 0]]) ) -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 @@ -84,9 +84,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 539383436a11..7a75b6094010 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from .t import TGate, TdgGate @@ -25,7 +25,7 @@ @with_gate_array(_H_ARRAY) -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 @@ -54,9 +54,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): """ @@ -94,8 +98,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) @@ -162,10 +165,21 @@ class CHGate(ControlledGate): \end{pmatrix} """ - 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 b47571290653..b8742665f66c 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -13,12 +13,12 @@ """Identity gate.""" from typing import Optional -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0], [0, 1]]) -class IGate(Gate): +class IGate(SingletonGate): r"""Identity gate. Identity gate corresponds to a single-qubit gate wait cycle, @@ -45,9 +45,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 a0cca1f484c7..018715ef3d68 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -16,7 +16,7 @@ import numpy as np -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -24,7 +24,7 @@ @with_gate_array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]]) -class iSwapGate(Gate): +class iSwapGate(SingletonGate): r"""iSWAP gate. A 2-qubit XX+YY interaction. @@ -85,9 +85,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 67b3aafbe786..07349eb96350 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -18,7 +18,7 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.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 from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -29,7 +29,7 @@ @with_gate_array(_S_ARRAY) -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). @@ -59,9 +59,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): """ @@ -90,7 +94,7 @@ def power(self, exponent: float): @with_gate_array(_SDG_ARRAY) -class SdgGate(Gate): +class SdgGate(SingletonGate): r"""Single qubit S-adjoint gate (~Z**0.5). It induces a :math:`-\pi/2` phase. @@ -120,9 +124,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 578efc3053fb..5f4cc76a87e1 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -24,7 +24,7 @@ @with_gate_array(_SWAP_ARRAY) -class SwapGate(Gate): +class SwapGate(SingletonGate): r"""The SWAP gate. This is a symmetric and Clifford gate. @@ -59,9 +59,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): """ @@ -103,8 +107,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) @@ -191,7 +194,12 @@ class CSwapGate(ControlledGate): |1, b, c\rangle \rightarrow |1, c, 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 CSWAP gate.""" super().__init__( "cswap", @@ -200,7 +208,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 a15b38787cc1..49fbb11c558c 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -16,7 +16,7 @@ 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 qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -26,7 +26,7 @@ @with_gate_array(_SX_ARRAY) -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` @@ -64,9 +64,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): """ @@ -108,14 +112,13 @@ 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) @with_gate_array(_SXDG_ARRAY) -class SXdgGate(Gate): +class SXdgGate(SingletonGate): r"""The inverse single-qubit Sqrt(X) gate. Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -148,9 +151,13 @@ class SXdgGate(Gate): ) _ARRAY.setflags(write=False) - 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): """ @@ -230,10 +237,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 67a14a12e1c5..e81d33773798 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -17,14 +17,14 @@ 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 from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) -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 @@ -55,9 +55,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): """ @@ -86,7 +90,7 @@ def power(self, exponent: float): @with_gate_array([[1, 0], [0, (1 - 1j) / math.sqrt(2)]]) -class TdgGate(Gate): +class TdgGate(SingletonGate): r"""Single qubit T-adjoint gate (~Z**0.25). It induces a :math:`-\pi/4` phase. @@ -116,9 +120,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 b48181be8062..cc17b8060ebe 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -17,7 +17,7 @@ import numpy from qiskit.utils.deprecation import deprecate_func from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array from .h import HGate @@ -30,7 +30,7 @@ @with_gate_array(_X_ARRAY) -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` @@ -76,9 +76,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): """ @@ -115,8 +119,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): @@ -188,10 +196,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 control( @@ -213,8 +232,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): @@ -291,10 +314,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): @@ -359,8 +393,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): @@ -380,7 +418,7 @@ def inverse(self): [0, 0, 0, 1j, 0, 0, 0, 0], ] ) -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. @@ -396,9 +434,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): """ @@ -675,7 +717,7 @@ def inverse(self): [0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0], ] ) -class RC3XGate(Gate): +class RC3XGate(SingletonGate): """The simplified 3-controlled Toffoli gate. The simplified Toffoli gate implements the Toffoli gate up to relative phases. @@ -689,9 +731,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): """ @@ -855,6 +901,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. @@ -866,9 +913,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) @@ -878,6 +927,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) @@ -888,7 +938,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): @@ -963,6 +1013,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 65be3087c8bb..b4e9509903e5 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -17,7 +17,7 @@ # pylint: disable=cyclic-import from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -25,7 +25,7 @@ @with_gate_array(_Y_ARRAY) -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` @@ -71,9 +71,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 @@ -108,8 +112,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) @@ -175,10 +178,21 @@ class CYGate(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 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 978688d1908c..9d8ba19d5fb1 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -19,7 +19,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .p import PhaseGate @@ -28,7 +28,7 @@ @with_gate_array(_Z_ARRAY) -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` @@ -74,9 +74,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 @@ -112,8 +116,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 b8ceb36496a7..d8c863a81d6f 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -193,7 +193,7 @@ def random_circuit( if is_cond: qc.measure(qc.qubits, cr) # 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..3d24247e31ca --- /dev/null +++ b/qiskit/circuit/singleton_gate.py @@ -0,0 +1,188 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Singleton gate classes. +""" +import copy + +from qiskit.circuit.gate import Gate +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit.exceptions import CircuitError + + +SINGLETONGATE_ATTR_SET = frozenset( + ( + "definition", + "unit", + "duration", + "condition", + "label", + "_label", + "_condition", + "_duration", + "_unit", + "_definition", + "_name", + "_num_qubits", + "_num_clbits", + "_params", + "params", + ) +) + + +class SingletonGate(Gate): + """A base class to use for Gate objects that by default are singleton instances + + This class should be used for gate classes that have fixed definitions and + do not contain any unique state. The canonical example of something like + this is :class:`~.HGate` which has an immutable definition and any + instance of :class:`~.HGate` is the same. Using singleton gates + as a base class for these types of gate classes provides a large + advantage in the memory footprint of multiple gates. + + The exception to be aware of with this class though are the :class:`~.Gate` + attributes :attr:`.label`, :attr:`.condition`, :attr:`.duration`, and + :attr:`.unit` which can be set differently for specific instances of gates. + For :class:`~.SingletonGate` usage to be sound setting these attributes + is not available and they can only be set at creation time. If any of these + attributes are used, then instead of using a single shared global instance + of the same gate a new separate instance will be created. + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if args or ( # pylint: disable=too-many-boolean-expressions + kwargs + and ( + "label" in kwargs + or "_condition" in kwargs + or "duration" in kwargs + or "unit" in kwargs + ) + ): + return super().__new__(cls) + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, *args, _condition=None, **kwargs): + super().__init__(*args, **kwargs) + self._condition = _condition + + def c_if(self, classical, val): + if not isinstance(classical, (ClassicalRegister, Clbit)): + raise CircuitError("c_if must be used with a classical register or classical bit") + if val < 0: + raise CircuitError("condition value should be non-negative") + if isinstance(classical, Clbit): + # Casting the conditional value as Boolean when + # the classical condition is on a classical bit. + val = bool(val) + instance = type(self)( + label=self.label, _condition=(classical, val), duration=self.duration, unit=self.unit + ) + return instance + + @property + def mutable(self) -> bool: + return self is not self._instance + + def to_mutable(self): + if not self.mutable: + instance = super().__new__(type(self)) + # Coming from a shared singleton none of the arguments to + # __init__ can be set, so this is the correct behavior for + # initializing a new mutable instance + instance.__init__() + return instance + else: + return copy.deepcopy(self) + + @property + def label(self) -> str: + return self._label + + @label.setter + def label(self, label: str): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "label on an instance. Instead you must set the label when instantiating a new object." + ) + self._label = label + + @property + def condition(self): + return self._condition + + @condition.setter + def condition(self, condition): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "condition on an instance. Instead you must set the label when instantiating a new " + "object or via the .c_if() method" + ) + self._condition = condition + + @property + def duration(self): + return self._duration + + @duration.setter + def duration(self, duration): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "duration on an instance. Instead you must set the duration when instantiating a " + "new object." + ) + self._duration = duration + + @property + def unit(self): + return self._unit + + @unit.setter + def unit(self, unit): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "unit on an instance. Instead you must set the unit when instantiating a " + "new object." + ) + self._unit = unit + + def __deepcopy__(self, _memo=None): + if not self.mutable: + return self + else: + return type(self)( + label=self.label, _condition=self.condition, duration=self.duration, unit=self.unit + ) + + def __setattr__(self, name, value): + if self.mutable: + super().__setattr__(name, value) + else: + if name not in SINGLETONGATE_ATTR_SET: + raise NotImplementedError( + "Setting custom attributes is not allowed on a singleton gate" + ) + super().__setattr__(name, value) + + def copy(self, name=None): + if not self.mutable and name is None: + return self + return super().copy(name=name) diff --git a/qiskit/converters/ast_to_dag.py b/qiskit/converters/ast_to_dag.py index 76541d8c89df..9fb3361d9555 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]], [], check=False) 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], check=False) 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]], [], check=False) 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, [], check=False) 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 f60223698e02..e11b218efba7 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -833,7 +833,10 @@ def _reject_new_register(reg): m_cargs = [edge_map.get(x, x) for x in nd.cargs] op = nd.op.copy() if (condition := getattr(op, "condition", None)) is not None: - op.condition = variable_mapper.map_condition(condition, allow_reorder=True) + if not isinstance(op, ControlFlowOp): + op = op.c_if(*variable_mapper.map_condition(condition, allow_reorder=True)) + else: + op.condition = variable_mapper.map_condition(condition, allow_reorder=True) elif isinstance(op, SwitchCaseOp): op.target = variable_mapper.map_target(op.target) dag.apply_operation_back(op, m_qargs, m_cargs, check=False) @@ -1272,7 +1275,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, check=False) else: in_dag = input_dag @@ -1356,8 +1363,13 @@ def edge_weight_map(wire): label=old_node.op.label, ) elif getattr(old_node.op, "condition", None) is not None: - m_op = copy.copy(old_node.op) - m_op.condition = variable_mapper.map_condition(m_op.condition) + 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: + m_op = m_op.c_if(*new_condition) + else: + m_op.condition = variable_mapper.map_condition(m_op.condition) else: m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] @@ -1430,7 +1442,10 @@ def substitute_node(self, node, op, inplace=False, propagate_condition=True): if (old_condition := getattr(node.op, "condition", None)) is not None: if not isinstance(op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") - op.condition = old_condition + if not isinstance(node.op, ControlFlowOp): + op = op.c_if(*old_condition) + else: + op.condition = old_condition new_wires.update(condition_resources(old_condition).clbits) if new_wires != current_wires: 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 5266bbb0346b..91fe62f3e368 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -25,10 +25,11 @@ from qiskit import circuit as circuit_mod from qiskit import extensions -from qiskit.circuit import library, controlflow, CircuitInstruction +from qiskit.circuit import library, controlflow, CircuitInstruction, ControlFlowOp from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -272,8 +273,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, *params) + gate = gate_class(condition, *params, label=label) elif version >= 5 and issubclass(gate_class, ControlledGate): if gate_name in { "MCPhaseGate", @@ -283,9 +286,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 @@ -304,10 +307,19 @@ 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 - if instruction.label_size > 0: - 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: + if not isinstance(gate, ControlFlowOp): + gate = gate.c_if(*condition) + else: + gate.condition = condition 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 7e5245b2e3fc..bc606a0f7161 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -192,8 +192,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): + gate = gate.to_mutable() + self._dd_sequence[index] = gate 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 fb33361a4959..2d4114f3cfc7 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -224,7 +224,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. params = self._resolve_params(gate) @@ -246,6 +246,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. + 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 f83aed800009..3792a149fd71 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,6 +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) + 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 a176f0aa5c9a..c75e22f285b8 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -96,13 +96,14 @@ def run(self, dag: DAGCircuit): # Make units consistent for node in dag.op_nodes(): try: - node.op = node.op.copy() - node.op.duration = self.inst_durations.get( + duration = self.inst_durations.get( node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit ) - node.op.unit = time_unit except TranspilerError: - pass + continue + node.op = node.op.to_mutable() + node.op.duration = duration + node.op.unit = time_unit 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..db6b761a392a --- /dev/null +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -0,0 +1,122 @@ +--- +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 constructor + will be a separate instance with the custom state instead of the globally + shared instance. You can also use the :meth:`.SingletonGate.to_mutable` + method to get a mutable copy of a gate object and then mutate the attributes + like you would on any other :class:`~.circuit.Instruction` object. + - | + 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. + - | + 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: + + * :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 instantiating an object + anymore. You will now only be able to set these attributes as arguments + when creating a new object or in the case of :attr:`~.Gate.condtion` through + the use :meth:`~.Gate.c_if`. Alternatively you can use :meth:`~.Gate.to_mutable` + 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 + :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 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 + :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 + :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/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: diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 1e2f5263ce34..825e46a8ddbc 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -609,8 +609,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 0c41189fc954..9589a68c7614 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", "_U0Gate", "_DefinedGate", } diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 8c8e9866050f..f5f5e14d5130 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 def test_deprecation_warnings_qasm_methods(self): diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py new file mode 100644 index 000000000000..d8c80661d675 --- /dev/null +++ b/test/python/circuit/test_singleton_gate.py @@ -0,0 +1,253 @@ +# 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, SXGate +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)) + + 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) + + 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) + + 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") diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 7f7375178bb0..35576d9b4361 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -477,8 +477,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]) @@ -490,8 +489,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]) @@ -509,8 +507,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,)) @@ -550,8 +547,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,)) @@ -596,8 +592,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,)) @@ -1153,8 +1148,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]) @@ -1167,8 +1161,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]) @@ -1210,8 +1203,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]) @@ -1226,8 +1218,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]) @@ -1305,8 +1296,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) @@ -2135,10 +2125,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)