Skip to content

Commit d62f780

Browse files
authored
Use singleton matrices for unparametrised standard gates (Qiskit#10296)
* Use singleton matrices for unparametrised standard gates This makes the array form of standard gates with zero parameters singleton class attributes that reject modification. The class-level `__array__` methods are updated to return exactly the same instance, except in very unusual circumstances, which means that `Gate.to_matrix()` and `numpy.asarray()` calls on the objects will return the same instance. This avoids a decent amount of construction time, and avoids several Python-space list allocations and array allocations. The dtypes of the static arrays are all standardised to by complex128. Gate matrices are in general unitary, `Gate.to_matrix()` already enforces a cast to `complex128`. For gates that allowed their dtypes to be inferred, there were several cases where native ints and floats would be used, meaning that `Gate.to_matrix()` would also involve an extra matrix allocation to hold the cast, which just wasted time. For standard controlled gates, we store both the closed- and open-controlled matrices singly controlled gates. For gates with more than one control, we only store the "all ones" controlled case, as a memory/speed trade-off; open controls are much less common than closed controls. For the most part this won't have an effect on peak memory usage, since all the allocated matrices in standard Qiskit usage would be freed by the garbage collector almost immediately. This will, however, reduce construction costs and garbage-collector pressure, since fewer allocations+frees will occur, and no calculations will need to be done. * Store only all-ones controls for large matrices * Fix lint * Use metaprogramming decorator to make `__array__` methods Instead of defining the array functions manually for each class, this adds a small amount of metaprogramming that adds them in with the correct `ndarray` properties set, including for controlled gates.
1 parent 5ab231d commit d62f780

File tree

13 files changed

+156
-301
lines changed

13 files changed

+156
-301
lines changed

qiskit/circuit/_utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,63 @@ def _ctrl_state_to_int(ctrl_state, num_ctrl_qubits):
9595
else:
9696
raise CircuitError(f"invalid control state specification: {repr(ctrl_state)}")
9797
return ctrl_state_std
98+
99+
100+
def with_gate_array(base_array):
101+
"""Class decorator that adds an ``__array__`` method to a :class:`.Gate` instance that returns a
102+
singleton nonwritable view onto the complex matrix described by ``base_array``."""
103+
nonwritable = numpy.array(base_array, dtype=numpy.complex128)
104+
nonwritable.setflags(write=False)
105+
106+
def __array__(_self, dtype=None):
107+
return numpy.asarray(nonwritable, dtype=dtype)
108+
109+
def decorator(cls):
110+
if hasattr(cls, "__array__"):
111+
raise RuntimeError("Refusing to decorate a class that already has '__array__' defined.")
112+
cls.__array__ = __array__
113+
return cls
114+
115+
return decorator
116+
117+
118+
def with_controlled_gate_array(base_array, num_ctrl_qubits, cached_states=None):
119+
"""Class decorator that adds an ``__array__`` method to a :class:`.ControlledGate` instance that
120+
returns singleton nonwritable views onto a relevant precomputed complex matrix for the given
121+
control state.
122+
123+
If ``cached_states`` is not given, then all possible control states are precomputed. If it is
124+
given, it should be an iterable of integers, and only these control states will be cached."""
125+
base = numpy.asarray(base_array, dtype=numpy.complex128)
126+
127+
def matrix_for_control_state(state):
128+
out = numpy.asarray(
129+
_compute_control_matrix(base, num_ctrl_qubits, state),
130+
dtype=numpy.complex128,
131+
)
132+
out.setflags(write=False)
133+
return out
134+
135+
if cached_states is None:
136+
nonwritables = [matrix_for_control_state(state) for state in range(2**num_ctrl_qubits)]
137+
138+
def __array__(self, dtype=None):
139+
return numpy.asarray(nonwritables[self.ctrl_state], dtype=dtype)
140+
141+
else:
142+
nonwritables = {state: matrix_for_control_state(state) for state in cached_states}
143+
144+
def __array__(self, dtype=None):
145+
if (out := nonwritables.get(self.ctrl_state)) is not None:
146+
return numpy.asarray(out, dtype=dtype)
147+
return numpy.asarray(
148+
_compute_control_matrix(base, num_ctrl_qubits, self.ctrl_state), dtype=dtype
149+
)
150+
151+
def decorator(cls):
152+
if hasattr(cls, "__array__"):
153+
raise RuntimeError("Refusing to decorate a class that already has '__array__' defined.")
154+
cls.__array__ = __array__
155+
return cls
156+
157+
return decorator

qiskit/circuit/library/standard_gates/dcx.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
"""Double-CNOT gate."""
1414

15-
import numpy as np
1615
from qiskit.circuit.gate import Gate
1716
from qiskit.circuit.quantumregister import QuantumRegister
17+
from qiskit.circuit._utils import with_gate_array
1818

1919

20+
@with_gate_array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]])
2021
class DCXGate(Gate):
2122
r"""Double-CNOT gate.
2223
@@ -66,7 +67,3 @@ def _define(self):
6667
qc._append(instr, qargs, cargs)
6768

6869
self.definition = qc
69-
70-
def __array__(self, dtype=None):
71-
"""Return a numpy.array for the DCX gate."""
72-
return np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=dtype)

qiskit/circuit/library/standard_gates/ecr.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
from math import sqrt
1515
import numpy as np
1616

17+
from qiskit.circuit._utils import with_gate_array
1718
from qiskit.circuit.gate import Gate
1819
from qiskit.circuit.quantumregister import QuantumRegister
1920
from .rzx import RZXGate
2021
from .x import XGate
2122

2223

24+
@with_gate_array(
25+
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]])
26+
)
2327
class ECRGate(Gate):
2428
r"""An echoed cross-resonance gate.
2529
@@ -106,14 +110,3 @@ def _define(self):
106110
def inverse(self):
107111
"""Return inverse ECR gate (itself)."""
108112
return ECRGate() # self-inverse
109-
110-
def to_matrix(self):
111-
"""Return a numpy.array for the ECR gate."""
112-
return (
113-
1
114-
/ sqrt(2)
115-
* np.array(
116-
[[0, 1, 0, 1.0j], [1, 0, -1.0j, 0], [0, 1.0j, 0, 1], [-1.0j, 0, 1, 0]],
117-
dtype=complex,
118-
)
119-
)

qiskit/circuit/library/standard_gates/h.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717
from qiskit.circuit.controlledgate import ControlledGate
1818
from qiskit.circuit.gate import Gate
1919
from qiskit.circuit.quantumregister import QuantumRegister
20+
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
2021
from .t import TGate, TdgGate
2122
from .s import SGate, SdgGate
2223

24+
_H_ARRAY = 1 / sqrt(2) * numpy.array([[1, 1], [1, -1]], dtype=numpy.complex128)
2325

26+
27+
@with_gate_array(_H_ARRAY)
2428
class HGate(Gate):
2529
r"""Single-qubit Hadamard gate.
2630
@@ -99,11 +103,8 @@ def inverse(self):
99103
r"""Return inverted H gate (itself)."""
100104
return HGate() # self-inverse
101105

102-
def __array__(self, dtype=None):
103-
"""Return a Numpy.array for the H gate."""
104-
return numpy.array([[1, 1], [1, -1]], dtype=dtype) / numpy.sqrt(2)
105-
106106

107+
@with_controlled_gate_array(_H_ARRAY, num_ctrl_qubits=1)
107108
class CHGate(ControlledGate):
108109
r"""Controlled-Hadamard gate.
109110
@@ -160,16 +161,6 @@ class CHGate(ControlledGate):
160161
0 & 0 & \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}}
161162
\end{pmatrix}
162163
"""
163-
# Define class constants. This saves future allocation time.
164-
_sqrt2o2 = 1 / sqrt(2)
165-
_matrix1 = numpy.array(
166-
[[1, 0, 0, 0], [0, _sqrt2o2, 0, _sqrt2o2], [0, 0, 1, 0], [0, _sqrt2o2, 0, -_sqrt2o2]],
167-
dtype=complex,
168-
)
169-
_matrix0 = numpy.array(
170-
[[_sqrt2o2, 0, _sqrt2o2, 0], [0, 1, 0, 0], [_sqrt2o2, 0, -_sqrt2o2, 0], [0, 0, 0, 1]],
171-
dtype=complex,
172-
)
173164

174165
def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[int, str]] = None):
175166
"""Create new CH gate."""
@@ -212,10 +203,3 @@ def _define(self):
212203
def inverse(self):
213204
"""Return inverted CH gate (itself)."""
214205
return CHGate(ctrl_state=self.ctrl_state) # self-inverse
215-
216-
def __array__(self, dtype=None):
217-
"""Return a numpy.array for the CH gate."""
218-
mat = self._matrix1 if self.ctrl_state else self._matrix0
219-
if dtype:
220-
return numpy.asarray(mat, dtype=dtype)
221-
return mat

qiskit/circuit/library/standard_gates/i.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
"""Identity gate."""
1414

1515
from typing import Optional
16-
import numpy
1716
from qiskit.circuit.gate import Gate
17+
from qiskit.circuit._utils import with_gate_array
1818

1919

20+
@with_gate_array([[1, 0], [0, 1]])
2021
class IGate(Gate):
2122
r"""Identity gate.
2223
@@ -52,10 +53,6 @@ def inverse(self):
5253
"""Invert this gate."""
5354
return IGate() # self-inverse
5455

55-
def __array__(self, dtype=None):
56-
"""Return a numpy.array for the identity gate."""
57-
return numpy.array([[1, 0], [0, 1]], dtype=dtype)
58-
5956
def power(self, exponent: float):
6057
"""Raise gate to a power."""
6158
return IGate()

qiskit/circuit/library/standard_gates/iswap.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
from qiskit.circuit.gate import Gate
2020
from qiskit.circuit.quantumregister import QuantumRegister
21+
from qiskit.circuit._utils import with_gate_array
2122

2223
from .xx_plus_yy import XXPlusYYGate
2324

2425

26+
@with_gate_array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])
2527
class iSwapGate(Gate):
2628
r"""iSWAP gate.
2729
@@ -120,10 +122,6 @@ def _define(self):
120122

121123
self.definition = qc
122124

123-
def __array__(self, dtype=None):
124-
"""Return a numpy.array for the iSWAP gate."""
125-
return np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], dtype=dtype)
126-
127125
def power(self, exponent: float):
128126
"""Raise gate to a power."""
129127
return XXPlusYYGate(-np.pi * exponent)

qiskit/circuit/library/standard_gates/s.py

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
from qiskit.circuit.gate import Gate
2222
from qiskit.circuit.library.standard_gates.p import CPhaseGate, PhaseGate
2323
from qiskit.circuit.quantumregister import QuantumRegister
24+
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
2425

2526

27+
_S_ARRAY = numpy.array([[1, 0], [0, 1j]])
28+
_SDG_ARRAY = numpy.array([[1, 0], [0, -1j]])
29+
30+
31+
@with_gate_array(_S_ARRAY)
2632
class SGate(Gate):
2733
r"""Single qubit S gate (Z**0.5).
2834
@@ -78,15 +84,12 @@ def inverse(self):
7884
"""Return inverse of S (SdgGate)."""
7985
return SdgGate()
8086

81-
def __array__(self, dtype=None):
82-
"""Return a numpy.array for the S gate."""
83-
return numpy.array([[1, 0], [0, 1j]], dtype=dtype)
84-
8587
def power(self, exponent: float):
8688
"""Raise gate to a power."""
8789
return PhaseGate(0.5 * numpy.pi * exponent)
8890

8991

92+
@with_gate_array(_SDG_ARRAY)
9093
class SdgGate(Gate):
9194
r"""Single qubit S-adjoint gate (~Z**0.5).
9295
@@ -142,15 +145,12 @@ def inverse(self):
142145
"""Return inverse of Sdg (SGate)."""
143146
return SGate()
144147

145-
def __array__(self, dtype=None):
146-
"""Return a numpy.array for the Sdg gate."""
147-
return numpy.array([[1, 0], [0, -1j]], dtype=dtype)
148-
149148
def power(self, exponent: float):
150149
"""Raise gate to a power."""
151150
return PhaseGate(-0.5 * numpy.pi * exponent)
152151

153152

153+
@with_controlled_gate_array(_S_ARRAY, num_ctrl_qubits=1)
154154
class CSGate(ControlledGate):
155155
r"""Controlled-S gate.
156156
@@ -179,23 +179,6 @@ class CSGate(ControlledGate):
179179
0 & 0 & 0 & i
180180
\end{pmatrix}
181181
"""
182-
# Define class constants. This saves future allocation time.
183-
_matrix1 = numpy.array(
184-
[
185-
[1, 0, 0, 0],
186-
[0, 1, 0, 0],
187-
[0, 0, 1, 0],
188-
[0, 0, 0, 1j],
189-
]
190-
)
191-
_matrix0 = numpy.array(
192-
[
193-
[1, 0, 0, 0],
194-
[0, 1, 0, 0],
195-
[0, 0, 1j, 0],
196-
[0, 0, 0, 1],
197-
]
198-
)
199182

200183
def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None):
201184
"""Create new CS gate."""
@@ -213,18 +196,12 @@ def inverse(self):
213196
"""Return inverse of CSGate (CSdgGate)."""
214197
return CSdgGate(ctrl_state=self.ctrl_state)
215198

216-
def __array__(self, dtype=None):
217-
"""Return a numpy.array for the CS gate."""
218-
mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0
219-
if dtype is not None:
220-
return numpy.asarray(mat, dtype=dtype)
221-
return mat
222-
223199
def power(self, exponent: float):
224200
"""Raise gate to a power."""
225201
return CPhaseGate(0.5 * numpy.pi * exponent)
226202

227203

204+
@with_controlled_gate_array(_SDG_ARRAY, num_ctrl_qubits=1)
228205
class CSdgGate(ControlledGate):
229206
r"""Controlled-S^\dagger gate.
230207
@@ -253,23 +230,6 @@ class CSdgGate(ControlledGate):
253230
0 & 0 & 0 & -i
254231
\end{pmatrix}
255232
"""
256-
# Define class constants. This saves future allocation time.
257-
_matrix1 = numpy.array(
258-
[
259-
[1, 0, 0, 0],
260-
[0, 1, 0, 0],
261-
[0, 0, 1, 0],
262-
[0, 0, 0, -1j],
263-
]
264-
)
265-
_matrix0 = numpy.array(
266-
[
267-
[1, 0, 0, 0],
268-
[0, 1, 0, 0],
269-
[0, 0, -1j, 0],
270-
[0, 0, 0, 1],
271-
]
272-
)
273233

274234
def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None):
275235
"""Create new CSdg gate."""
@@ -293,13 +253,6 @@ def inverse(self):
293253
"""Return inverse of CSdgGate (CSGate)."""
294254
return CSGate(ctrl_state=self.ctrl_state)
295255

296-
def __array__(self, dtype=None):
297-
"""Return a numpy.array for the CSdg gate."""
298-
mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0
299-
if dtype is not None:
300-
return numpy.asarray(mat, dtype=dtype)
301-
return mat
302-
303256
def power(self, exponent: float):
304257
"""Raise gate to a power."""
305258
return CPhaseGate(-0.5 * numpy.pi * exponent)

0 commit comments

Comments
 (0)