From d1f6850d8ddd64f6d17bc7468fef71dbec2e59d0 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 8 Apr 2024 09:00:50 +0100 Subject: [PATCH 1/7] Fix bad release-notes section (#12153) --- .../1.0/add-annotated-arg-to-control-d9a188fe66f037ad.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/1.0/add-annotated-arg-to-control-d9a188fe66f037ad.yaml b/releasenotes/notes/1.0/add-annotated-arg-to-control-d9a188fe66f037ad.yaml index c4bfbac46ca4..c86f1f68d427 100644 --- a/releasenotes/notes/1.0/add-annotated-arg-to-control-d9a188fe66f037ad.yaml +++ b/releasenotes/notes/1.0/add-annotated-arg-to-control-d9a188fe66f037ad.yaml @@ -1,5 +1,5 @@ --- -features_circuit: +features_circuits: - | Added a new argument, ``annotated``, to the methods :meth:`.QuantumCircuit.control`, :meth:`.Gate.control` and ``.control()`` methods of :class:`.Gate` subclasses (such as :class:`.UnitaryGate` or :class:`.SwapGate`) From 9ed963f1aad18d518b43306cb63f8d3e1c3d0382 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:59:38 +0000 Subject: [PATCH 2/7] Bump ahash from 0.8.6 to 0.8.11 (#12154) Bumps [ahash](https://github.com/tkaitchuck/ahash) from 0.8.6 to 0.8.11. - [Release notes](https://github.com/tkaitchuck/ahash/releases) - [Commits](https://github.com/tkaitchuck/ahash/commits/v0.8.11) --- updated-dependencies: - dependency-name: ahash dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jake Lishman --- Cargo.lock | 4 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03f74d03aedb..cdd703e02316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 12e3c589192f..dca8fda1f9e2 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ numpy = "0.21.0" rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" -ahash = "0.8.6" +ahash = "0.8.11" num-traits = "0.2" num-complex = "0.4" num-bigint = "0.4" From 44cbb7cec945d76b41431ea2781f8f343bd13fd2 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 8 Apr 2024 12:48:41 +0100 Subject: [PATCH 3/7] Add `copy` keyword argument to `QuantumCircuit.append` (#12098) We always copy compile-time parametric instructions during `QuantumCircuit.append` to avoid accidental mutation of other references in a call to `assign_parameters(inplace=True)`. However, mostly our rotation gates are added through things like `QuantumCircuit.rz`, where the only reference to the `RZGate` is internal to the `QuantumCircuit`, so creating and immediately copying it is a waste of time. This commit adds a `copy` argument to `QuantumCircuit.append`, so we can set it to `False` in places where we know we own the instruction being added. --- qiskit/circuit/library/blueprintcircuit.py | 6 +- qiskit/circuit/library/phase_oracle.py | 2 +- qiskit/circuit/quantumcircuit.py | 205 ++++++++++++------ ...mcircuit-append-copy-8a9b71ad4b789490.yaml | 21 ++ test/python/circuit/test_compose.py | 20 ++ test/python/circuit/test_parameters.py | 23 ++ 6 files changed, 203 insertions(+), 74 deletions(-) create mode 100644 releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 31ffae66f0b6..f6c3404e38c4 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -127,10 +127,12 @@ def _append(self, instruction, _qargs=None, _cargs=None): self._build() return super()._append(instruction, _qargs, _cargs) - def compose(self, other, qubits=None, clbits=None, front=False, inplace=False, wrap=False): + def compose( + self, other, qubits=None, clbits=None, front=False, inplace=False, wrap=False, *, copy=True + ): if not self._is_built: self._build() - return super().compose(other, qubits, clbits, front, inplace, wrap) + return super().compose(other, qubits, clbits, front, inplace, wrap, copy=copy) def inverse(self, annotated: bool = False): if not self._is_built: diff --git a/qiskit/circuit/library/phase_oracle.py b/qiskit/circuit/library/phase_oracle.py index f00a8d56255a..355c0a69f85c 100644 --- a/qiskit/circuit/library/phase_oracle.py +++ b/qiskit/circuit/library/phase_oracle.py @@ -87,7 +87,7 @@ def synthesizer(boolean_expression): super().__init__(oracle.num_qubits, name="Phase Oracle") - self.compose(oracle, inplace=True) + self.compose(oracle, inplace=True, copy=False) def evaluate_bitstring(self, bitstring: str) -> bool: """Evaluate the oracle on a bitstring. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ed170dde6cb2..56a283652f01 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -15,7 +15,7 @@ """Quantum circuit object.""" from __future__ import annotations -import copy +import copy as _copy import itertools import multiprocessing as mp import typing @@ -425,10 +425,10 @@ def data(self, data_input: Iterable): return if isinstance(data_input[0], CircuitInstruction): for instruction in data_input: - self.append(instruction) + self.append(instruction, copy=False) else: for instruction, qargs, cargs in data_input: - self.append(instruction, qargs, cargs) + self.append(instruction, qargs, cargs, copy=False) @property def op_start_times(self) -> list[int]: @@ -535,7 +535,7 @@ def __deepcopy__(self, memo=None): cls = self.__class__ result = cls.__new__(cls) for k in self.__dict__.keys() - {"_data", "_builder_api"}: - setattr(result, k, copy.deepcopy(self.__dict__[k], memo)) + setattr(result, k, _copy.deepcopy(self.__dict__[k], memo)) result._builder_api = _OuterCircuitScopeInterface(result) @@ -543,10 +543,10 @@ def __deepcopy__(self, memo=None): # like we would when pickling. result._data = self._data.copy() result._data.replace_bits( - qubits=copy.deepcopy(self._data.qubits, memo), - clbits=copy.deepcopy(self._data.clbits, memo), + qubits=_copy.deepcopy(self._data.qubits, memo), + clbits=_copy.deepcopy(self._data.clbits, memo), ) - result._data.map_ops(lambda op: copy.deepcopy(op, memo)) + result._data.map_ops(lambda op: _copy.deepcopy(op, memo)) return result @classmethod @@ -869,6 +869,8 @@ def compose( front: bool = False, inplace: bool = False, wrap: bool = False, + *, + copy: bool = True, ) -> Optional["QuantumCircuit"]: """Compose circuit with ``other`` circuit or instruction, optionally permuting wires. @@ -885,6 +887,12 @@ def compose( inplace (bool): If True, modify the object. Otherwise return composed circuit. wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on whether it contains only unitary instructions) before composing it onto self. + copy (bool): If ``True`` (the default), then the input is treated as shared, and any + contained instructions will be copied, if they might need to be mutated in the + future. You can set this to ``False`` if the input should be considered owned by + the base circuit, in order to avoid unnecessary copies; in this case, it is not + valid to use ``other`` afterwards, and some instructions may have been mutated in + place. Returns: QuantumCircuit: the composed circuit (returns None if inplace==True). @@ -969,11 +977,11 @@ def compose( # Need to keep a reference to the data for use after we've emptied it. old_data = dest._data.copy() dest.clear() - dest.append(other, qubits, clbits) + dest.append(other, qubits, clbits, copy=copy) for instruction in old_data: dest._append(instruction) else: - dest.append(other, qargs=qubits, cargs=clbits) + dest.append(other, qargs=qubits, cargs=clbits, copy=copy) return None if inplace else dest if other.num_qubits > dest.num_qubits or other.num_clbits > dest.num_clbits: @@ -1035,10 +1043,11 @@ def compose( ) def map_vars(op): - n_op = op.copy() + n_op = op.copy() if copy else op if (condition := getattr(n_op, "condition", None)) is not None: n_op.condition = variable_mapper.map_condition(condition) if isinstance(n_op, SwitchCaseOp): + n_op = n_op.copy() if n_op is op else n_op n_op.target = variable_mapper.map_target(n_op.target) return n_op @@ -1312,6 +1321,8 @@ def append( instruction: Operation | CircuitInstruction, qargs: Sequence[QubitSpecifier] | None = None, cargs: Sequence[ClbitSpecifier] | None = None, + *, + copy: bool = True, ) -> InstructionSet: """Append one or more instructions to the end of the circuit, modifying the circuit in place. @@ -1329,6 +1340,11 @@ def append( :class:`.CircuitInstruction` with all its context. qargs: specifiers of the :class:`~.circuit.Qubit`\\ s to attach instruction to. cargs: specifiers of the :class:`.Clbit`\\ s to attach instruction to. + copy: if ``True`` (the default), then the incoming ``instruction`` is copied before + adding it to the circuit if it contains symbolic parameters, so it can be safely + mutated without affecting other circuits the same instruction might be in. If you + are sure this instruction will not be in other circuits, you can set this ``False`` + for a small speedup. Returns: qiskit.circuit.InstructionSet: a handle to the :class:`.CircuitInstruction`\\ s that @@ -1367,11 +1383,11 @@ def append( if params := getattr(operation, "params", ()): is_parameter = False for param in params: - is_parameter = is_parameter or isinstance(param, Parameter) + is_parameter = is_parameter or isinstance(param, ParameterExpression) if isinstance(param, expr.Expr): param = _validate_expr(circuit_scope, param) - if is_parameter: - operation = copy.deepcopy(operation) + if copy and is_parameter: + operation = _copy.deepcopy(operation) if isinstance(operation, ControlFlowOp): # Verify that any variable bindings are valid. Control-flow ops are already enforced # by the class not to contain 'input' variables. @@ -2543,7 +2559,7 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": raise TypeError( f"invalid name for a circuit: '{name}'. The name must be a string or 'None'." ) - cpy = copy.copy(self) + cpy = _copy.copy(self) # copy registers correctly, in copy.copy they are only copied via reference cpy.qregs = self.qregs.copy() cpy.cregs = self.cregs.copy() @@ -2566,8 +2582,8 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": ) cpy._data = CircuitData(self._data.qubits, self._data.clbits) - cpy._calibrations = copy.deepcopy(self._calibrations) - cpy._metadata = copy.deepcopy(self._metadata) + cpy._calibrations = _copy.deepcopy(self._calibrations) + cpy._metadata = _copy.deepcopy(self._metadata) if name: cpy.name = name @@ -2616,7 +2632,7 @@ def reset(self, qubit: QubitSpecifier) -> InstructionSet: """ from .reset import Reset - return self.append(Reset(), [qubit], []) + return self.append(Reset(), [qubit], [], copy=False) def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: """Store the result of the given real-time classical expression ``rvalue`` in the memory @@ -2641,7 +2657,7 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: :meth:`add_var` Create a new variable in the circuit that can be written to with this method. """ - return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), ()) + return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), (), copy=False) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). @@ -2718,7 +2734,7 @@ def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet """ from .measure import Measure - return self.append(Measure(), [qubit], [cbit]) + return self.append(Measure(), [qubit], [cbit], copy=False) def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: """Adds measurement to all non-idle qubits. Creates a new ClassicalRegister with @@ -3294,7 +3310,9 @@ def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: if qargs: # This uses a `dict` not a `set` to guarantee a deterministic order to the arguments. qubits = tuple({q: None for qarg in qargs for q in self.qbit_argument_conversion(qarg)}) - return self.append(CircuitInstruction(Barrier(len(qubits), label=label), qubits, ())) + return self.append( + CircuitInstruction(Barrier(len(qubits), label=label), qubits, ()), copy=False + ) else: qubits = self.qubits.copy() return self._current_scope().append( @@ -3325,7 +3343,7 @@ def delay( """ if qarg is None: qarg = self.qubits - return self.append(Delay(duration, unit=unit), [qarg], []) + return self.append(Delay(duration, unit=unit), [qarg], [], copy=False) def h(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.HGate`. @@ -3340,7 +3358,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.h import HGate - return self.append(HGate(), [qubit], []) + return self.append(HGate(), [qubit], [], copy=False) def ch( self, @@ -3367,7 +3385,10 @@ def ch( from .library.standard_gates.h import CHGate return self.append( - CHGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CHGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invalid-name @@ -3383,7 +3404,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali """ from .library.standard_gates.i import IGate - return self.append(IGate(), [qubit], []) + return self.append(IGate(), [qubit], [], copy=False) def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.MSGate`. @@ -3400,7 +3421,7 @@ def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> Ins # pylint: disable=cyclic-import from .library.generalized_gates.gms import MSGate - return self.append(MSGate(len(qubits), theta), qubits) + return self.append(MSGate(len(qubits), theta), qubits, copy=False) def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.PhaseGate`. @@ -3416,7 +3437,7 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.p import PhaseGate - return self.append(PhaseGate(theta), [qubit], []) + return self.append(PhaseGate(theta), [qubit], [], copy=False) def cp( self, @@ -3445,7 +3466,10 @@ def cp( from .library.standard_gates.p import CPhaseGate return self.append( - CPhaseGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CPhaseGate(theta, label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def mcp( @@ -3477,6 +3501,7 @@ def mcp( MCPhaseGate(lam, num_ctrl_qubits, ctrl_state=ctrl_state), control_qubits[:] + [target_qubit], [], + copy=False, ) def r( @@ -3496,7 +3521,7 @@ def r( """ from .library.standard_gates.r import RGate - return self.append(RGate(theta, phi), [qubit], []) + return self.append(RGate(theta, phi), [qubit], [], copy=False) def rv( self, @@ -3523,7 +3548,7 @@ def rv( """ from .library.generalized_gates.rv import RVGate - return self.append(RVGate(vx, vy, vz), [qubit], []) + return self.append(RVGate(vx, vy, vz), [qubit], [], copy=False) def rccx( self, @@ -3545,7 +3570,9 @@ def rccx( """ from .library.standard_gates.x import RCCXGate - return self.append(RCCXGate(), [control_qubit1, control_qubit2, target_qubit], []) + return self.append( + RCCXGate(), [control_qubit1, control_qubit2, target_qubit], [], copy=False + ) def rcccx( self, @@ -3570,7 +3597,10 @@ def rcccx( from .library.standard_gates.x import RC3XGate return self.append( - RC3XGate(), [control_qubit1, control_qubit2, control_qubit3, target_qubit], [] + RC3XGate(), + [control_qubit1, control_qubit2, control_qubit3, target_qubit], + [], + copy=False, ) def rx( @@ -3590,7 +3620,7 @@ def rx( """ from .library.standard_gates.rx import RXGate - return self.append(RXGate(theta, label=label), [qubit], []) + return self.append(RXGate(theta, label=label), [qubit], [], copy=False) def crx( self, @@ -3619,7 +3649,10 @@ def crx( from .library.standard_gates.rx import CRXGate return self.append( - CRXGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CRXGate(theta, label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def rxx( @@ -3639,7 +3672,7 @@ def rxx( """ from .library.standard_gates.rxx import RXXGate - return self.append(RXXGate(theta), [qubit1, qubit2], []) + return self.append(RXXGate(theta), [qubit1, qubit2], [], copy=False) def ry( self, theta: ParameterValueType, qubit: QubitSpecifier, label: str | None = None @@ -3658,7 +3691,7 @@ def ry( """ from .library.standard_gates.ry import RYGate - return self.append(RYGate(theta, label=label), [qubit], []) + return self.append(RYGate(theta, label=label), [qubit], [], copy=False) def cry( self, @@ -3687,7 +3720,10 @@ def cry( from .library.standard_gates.ry import CRYGate return self.append( - CRYGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CRYGate(theta, label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def ryy( @@ -3707,7 +3743,7 @@ def ryy( """ from .library.standard_gates.ryy import RYYGate - return self.append(RYYGate(theta), [qubit1, qubit2], []) + return self.append(RYYGate(theta), [qubit1, qubit2], [], copy=False) def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.RZGate`. @@ -3723,7 +3759,7 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.rz import RZGate - return self.append(RZGate(phi), [qubit], []) + return self.append(RZGate(phi), [qubit], [], copy=False) def crz( self, @@ -3752,7 +3788,10 @@ def crz( from .library.standard_gates.rz import CRZGate return self.append( - CRZGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CRZGate(theta, label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def rzx( @@ -3772,7 +3811,7 @@ def rzx( """ from .library.standard_gates.rzx import RZXGate - return self.append(RZXGate(theta), [qubit1, qubit2], []) + return self.append(RZXGate(theta), [qubit1, qubit2], [], copy=False) def rzz( self, theta: ParameterValueType, qubit1: QubitSpecifier, qubit2: QubitSpecifier @@ -3791,7 +3830,7 @@ def rzz( """ from .library.standard_gates.rzz import RZZGate - return self.append(RZZGate(theta), [qubit1, qubit2], []) + return self.append(RZZGate(theta), [qubit1, qubit2], [], copy=False) def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.ECRGate`. @@ -3806,7 +3845,7 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.ecr import ECRGate - return self.append(ECRGate(), [qubit1, qubit2], []) + return self.append(ECRGate(), [qubit1, qubit2], [], copy=False) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -3821,7 +3860,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.s import SGate - return self.append(SGate(), [qubit], []) + return self.append(SGate(), [qubit], [], copy=False) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -3836,7 +3875,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.s import SdgGate - return self.append(SdgGate(), [qubit], []) + return self.append(SdgGate(), [qubit], [], copy=False) def cs( self, @@ -3866,6 +3905,7 @@ def cs( CSGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [], + copy=False, ) def csdg( @@ -3896,6 +3936,7 @@ def csdg( CSdgGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [], + copy=False, ) def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -3911,7 +3952,7 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet """ from .library.standard_gates.swap import SwapGate - return self.append(SwapGate(), [qubit1, qubit2], []) + return self.append(SwapGate(), [qubit1, qubit2], [], copy=False) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.iSwapGate`. @@ -3926,7 +3967,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe """ from .library.standard_gates.iswap import iSwapGate - return self.append(iSwapGate(), [qubit1, qubit2], []) + return self.append(iSwapGate(), [qubit1, qubit2], [], copy=False) def cswap( self, @@ -3958,6 +3999,7 @@ def cswap( CSwapGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit1, target_qubit2], [], + copy=False, ) def sx(self, qubit: QubitSpecifier) -> InstructionSet: @@ -3973,7 +4015,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.sx import SXGate - return self.append(SXGate(), [qubit], []) + return self.append(SXGate(), [qubit], [], copy=False) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -3988,7 +4030,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.sx import SXdgGate - return self.append(SXdgGate(), [qubit], []) + return self.append(SXdgGate(), [qubit], [], copy=False) def csx( self, @@ -4018,6 +4060,7 @@ def csx( CSXGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [], + copy=False, ) def t(self, qubit: QubitSpecifier) -> InstructionSet: @@ -4033,7 +4076,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.t import TGate - return self.append(TGate(), [qubit], []) + return self.append(TGate(), [qubit], [], copy=False) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -4048,7 +4091,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.t import TdgGate - return self.append(TdgGate(), [qubit], []) + return self.append(TdgGate(), [qubit], [], copy=False) def u( self, @@ -4072,7 +4115,7 @@ def u( """ from .library.standard_gates.u import UGate - return self.append(UGate(theta, phi, lam), [qubit], []) + return self.append(UGate(theta, phi, lam), [qubit], [], copy=False) def cu( self, @@ -4110,6 +4153,7 @@ def cu( CUGate(theta, phi, lam, gamma, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [], + copy=False, ) def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: @@ -4126,7 +4170,7 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: """ from .library.standard_gates.x import XGate - return self.append(XGate(label=label), [qubit], []) + return self.append(XGate(label=label), [qubit], [], copy=False) def cx( self, @@ -4154,7 +4198,10 @@ def cx( from .library.standard_gates.x import CXGate return self.append( - CXGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CXGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -4171,7 +4218,7 @@ def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.dcx import DCXGate - return self.append(DCXGate(), [qubit1, qubit2], []) + return self.append(DCXGate(), [qubit1, qubit2], [], copy=False) def ccx( self, @@ -4201,6 +4248,7 @@ def ccx( CCXGate(ctrl_state=ctrl_state), [control_qubit1, control_qubit2, target_qubit], [], + copy=False, ) def mcx( @@ -4300,7 +4348,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.y import YGate - return self.append(YGate(), [qubit], []) + return self.append(YGate(), [qubit], [], copy=False) def cy( self, @@ -4327,7 +4375,10 @@ def cy( from .library.standard_gates.y import CYGate return self.append( - CYGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CYGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def z(self, qubit: QubitSpecifier) -> InstructionSet: @@ -4343,7 +4394,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: """ from .library.standard_gates.z import ZGate - return self.append(ZGate(), [qubit], []) + return self.append(ZGate(), [qubit], [], copy=False) def cz( self, @@ -4370,7 +4421,10 @@ def cz( from .library.standard_gates.z import CZGate return self.append( - CZGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] + CZGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def ccz( @@ -4403,6 +4457,7 @@ def ccz( CCZGate(label=label, ctrl_state=ctrl_state), [control_qubit1, control_qubit2, target_qubit], [], + copy=False, ) def pauli( @@ -4421,7 +4476,7 @@ def pauli( """ from qiskit.circuit.library.generalized_gates.pauli import PauliGate - return self.append(PauliGate(pauli_string), qubits, []) + return self.append(PauliGate(pauli_string), qubits, [], copy=False) def prepare_state( self, @@ -4532,7 +4587,9 @@ def prepare_state( num_qubits = len(qubits) if isinstance(state, int) else None return self.append( - StatePreparation(state, num_qubits, label=label, normalize=normalize), qubits + StatePreparation(state, num_qubits, label=label, normalize=normalize), + qubits, + copy=False, ) def initialize( @@ -4644,7 +4701,7 @@ class to prepare the qubits in a specified state. num_qubits = len(qubits) if isinstance(params, int) else None - return self.append(Initialize(params, num_qubits, normalize), qubits) + return self.append(Initialize(params, num_qubits, normalize), qubits, copy=False) def unitary( self, @@ -4687,7 +4744,7 @@ def unitary( if isinstance(qubits, (int, Qubit)) or len(qubits) > 1: qubits = [qubits] - return self.append(gate, qubits, []) + return self.append(gate, qubits, [], copy=False) def _current_scope(self) -> CircuitScopeInterface: if self._control_flow_scopes: @@ -4869,7 +4926,7 @@ def while_loop(self, condition, body=None, qubits=None, clbits=None, *, label=No "When using 'while_loop' with a body, you must pass qubits and clbits." ) - return self.append(WhileLoopOp(condition, body, label), qubits, clbits) + return self.append(WhileLoopOp(condition, body, label), qubits, clbits, copy=False) @typing.overload def for_loop( @@ -4958,7 +5015,9 @@ def for_loop( "When using 'for_loop' with a body, you must pass qubits and clbits." ) - return self.append(ForLoopOp(indexset, loop_parameter, body, label), qubits, clbits) + return self.append( + ForLoopOp(indexset, loop_parameter, body, label), qubits, clbits, copy=False + ) @typing.overload def if_test( @@ -5068,7 +5127,7 @@ def if_test( elif qubits is None or clbits is None: raise CircuitError("When using 'if_test' with a body, you must pass qubits and clbits.") - return self.append(IfElseOp(condition, true_body, None, label), qubits, clbits) + return self.append(IfElseOp(condition, true_body, None, label), qubits, clbits, copy=False) def if_else( self, @@ -5123,7 +5182,9 @@ def if_else( else: condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) - return self.append(IfElseOp(condition, true_body, false_body, label), qubits, clbits) + return self.append( + IfElseOp(condition, true_body, false_body, label), qubits, clbits, copy=False + ) @typing.overload def switch( @@ -5214,7 +5275,7 @@ def switch(self, target, cases=None, qubits=None, clbits=None, *, label=None): if qubits is None or clbits is None: raise CircuitError("When using 'switch' with cases, you must pass qubits and clbits.") - return self.append(SwitchCaseOp(target, cases, label=label), qubits, clbits) + return self.append(SwitchCaseOp(target, cases, label=label), qubits, clbits, copy=False) def break_loop(self) -> InstructionSet: """Apply :class:`~qiskit.circuit.BreakLoopOp`. @@ -5240,8 +5301,10 @@ def break_loop(self) -> InstructionSet: if self._control_flow_scopes: operation = BreakLoopPlaceholder() resources = operation.placeholder_resources() - return self.append(operation, resources.qubits, resources.clbits) - return self.append(BreakLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits) + return self.append(operation, resources.qubits, resources.clbits, copy=False) + return self.append( + BreakLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits, copy=False + ) def continue_loop(self) -> InstructionSet: """Apply :class:`~qiskit.circuit.ContinueLoopOp`. @@ -5267,9 +5330,9 @@ def continue_loop(self) -> InstructionSet: if self._control_flow_scopes: operation = ContinueLoopPlaceholder() resources = operation.placeholder_resources() - return self.append(operation, resources.qubits, resources.clbits) + return self.append(operation, resources.qubits, resources.clbits, copy=False) return self.append( - ContinueLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits + ContinueLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits, copy=False ) def add_calibration( diff --git a/releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml b/releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml new file mode 100644 index 000000000000..2f7280215671 --- /dev/null +++ b/releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml @@ -0,0 +1,21 @@ +--- +features_circuits: + - | + :meth:`.QuantumCircuit.append` now has a ``copy`` keyword argument, which defaults to ``True``. + When an instruction with runtime parameters (:class:`.ParameterExpression`\ s) is appended to + a circuit, by default, the circuit has always created a copy of the instruction so that if + :meth:`.QuantumCircuit.assign_parameters` attempts to mutate the instruction in place, it does + not affect other references to the same instruction. Now, setting ``copy=False`` allows you to + override this, so you can avoid the copy penalty if you know your instructions will not be used + in other locations. + - | + :meth:`.QuantumCircuit.compose` now has a ``copy`` keyword argument, which defaults to ``True``. + By default, :meth:`~.QuantumCircuit.compose` copies all instructions, so that mutations from one + circuit do not affect any other. If ``copy=False``, then instructions from the other circuit + will become directly owned by the new circuit, which may involve mutating them in place. The + other circuit must not be used afterwards, in this case. +fixes: + - | + :meth:`.QuantumCircuit.append` with ``copy=True`` (its default) will now correctly copy + instructions parametrized by :class:`.ParameterExpression` instances, and not just by + :class:`.Parameter` instances. diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 7cf2be598d95..03301899a6ae 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -345,6 +345,26 @@ def test_compose_permuted_smaller(self): self.assertEqual(circuit_composed, circuit_expected) + def test_compose_copy(self): + """Test that `compose` copies instructions where appropriate.""" + base = QuantumCircuit(2, 2) + + # If given a parametric instruction, the instruction should be copied in the output unless + # specifically set to take ownership. + parametric = QuantumCircuit(1) + parametric.rz(Parameter("x"), 0) + should_copy = base.compose(parametric, qubits=[0]) + self.assertIsNot(should_copy.data[-1].operation, parametric.data[-1].operation) + self.assertEqual(should_copy.data[-1].operation, parametric.data[-1].operation) + forbid_copy = base.compose(parametric, qubits=[0], copy=False) + self.assertIs(forbid_copy.data[-1].operation, parametric.data[-1].operation) + + conditional = QuantumCircuit(1, 1) + conditional.x(0).c_if(conditional.clbits[0], True) + test = base.compose(conditional, qubits=[0], clbits=[0], copy=False) + self.assertIs(test.data[-1].operation, conditional.data[-1].operation) + self.assertEqual(test.data[-1].operation.condition, (test.clbits[0], True)) + def test_compose_classical(self): """Composing on classical bits. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 9511ac08654c..7bcc2cd35f3a 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -145,6 +145,29 @@ def test_duplicate_name_on_append(self): qc.rx(param_a, 0) self.assertRaises(CircuitError, qc.rx, param_a_again, 0) + def test_append_copies_parametric(self): + """Test that `QuantumCircuit.append` copies instructions when they contain compile + parameters and expressions.""" + param = Parameter("a") + expr = param * 2 + gate_param = RZGate(param) + gate_expr = RZGate(expr) + + qc = QuantumCircuit(1) + qc.append(gate_param, [0], copy=True) + self.assertIsNot(qc.data[-1].operation, gate_param) + self.assertEqual(qc.data[-1].operation, gate_param) + + qc.append(gate_param, [0], copy=False) + self.assertIs(qc.data[-1].operation, gate_param) + + qc.append(gate_expr, [0], copy=True) + self.assertIsNot(qc.data[-1].operation, gate_expr) + self.assertEqual(qc.data[-1].operation, gate_expr) + + qc.append(gate_expr, [0], copy=False) + self.assertIs(qc.data[-1].operation, gate_expr) + def test_parameters_property(self): """Test instantiating gate with variable parameters""" from qiskit.circuit.library.standard_gates.rx import RXGate From c99f325baac1ea19ec4a316299579e9101e76271 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 8 Apr 2024 21:41:31 +0100 Subject: [PATCH 4/7] Improve performance and randomness of `QuantumVolume` (#12097) * Improve performance and randomness of `QuantumVolume` This hugely improves the construction time of `QuantumVolume` circuits, in part by removing the previous behaviour of using low-entropy indiviual RNG instances for each SU4 matrix. Now that we need larger circuits, this would already have been a non-trivial biasing of the random outputs, but also, calling Scipy random variates in a loop is _much_ less efficient than vectorising the entire process in one go. Along with changes to the RNG, this commit also adds a faster path to `UnitaryGate` to shave off some useless repeated calculations (this can likely be used elsewhere in Qiskit) and reworks the `QuantumVolume` builder to use more efficient circuit construction methods. * Make test explicitly test reproducibility * Protect best-effort seed retrieval against old Numpy --- .../library/generalized_gates/unitary.py | 5 +- qiskit/circuit/library/quantum_volume.py | 77 ++++++++++--------- .../notes/qv-perf-be76290f472e4777.yaml | 27 +++++++ .../circuit/library/test_quantum_volume.py | 24 +++--- 4 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 releasenotes/notes/qv-perf-be76290f472e4777.yaml diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index c065abbe7cd1..618041142227 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -70,6 +70,8 @@ def __init__( data: numpy.ndarray | Gate | BaseOperator, label: str | None = None, check_input: bool = True, + *, + num_qubits: int | None = None, ) -> None: """Create a gate from a numeric unitary matrix. @@ -81,6 +83,7 @@ def __init__( be skipped. This should only ever be used if you know the input is unitary, setting this to ``False`` and passing in a non-unitary matrix will result unexpected behavior and errors. + num_qubits: If given, the number of qubits in the matrix. If not given, it is inferred. Raises: ValueError: If input data is not an N-qubit unitary operator. @@ -97,7 +100,7 @@ def __init__( # Convert to numpy array in case not already an array data = numpy.asarray(data, dtype=complex) input_dim, output_dim = data.shape - num_qubits = int(math.log2(input_dim)) + num_qubits = num_qubits if num_qubits is not None else int(math.log2(input_dim)) if check_input: # Check input is unitary if not is_unitary_matrix(data): diff --git a/qiskit/circuit/library/quantum_volume.py b/qiskit/circuit/library/quantum_volume.py index 54a1b30dbec7..1c952808fa5a 100644 --- a/qiskit/circuit/library/quantum_volume.py +++ b/qiskit/circuit/library/quantum_volume.py @@ -15,9 +15,8 @@ from typing import Optional, Union import numpy as np -from qiskit.quantum_info.random import random_unitary -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library.generalized_gates.permutation import Permutation +from qiskit.circuit import QuantumCircuit, CircuitInstruction +from qiskit.circuit.library.generalized_gates import PermutationGate, UnitaryGate class QuantumVolume(QuantumCircuit): @@ -60,6 +59,8 @@ def __init__( depth: Optional[int] = None, seed: Optional[Union[int, np.random.Generator]] = None, classical_permutation: bool = True, + *, + flatten: bool = False, ) -> None: """Create quantum volume model circuit of size num_qubits x depth. @@ -69,46 +70,46 @@ def __init__( seed: Random number generator or generator seed. classical_permutation: use classical permutations at every layer, rather than quantum. + flatten: If ``False`` (the default), construct a circuit that contains a single + instruction, which in turn has the actual volume structure. If ``True``, construct + the volume structure directly. """ - # Initialize RNG - if seed is None: - rng_set = np.random.default_rng() - seed = rng_set.integers(low=1, high=1000) - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) + import scipy.stats # Parameters depth = depth or num_qubits # how many layers of SU(4) - width = int(np.floor(num_qubits / 2)) # how many SU(4)s fit in each layer - name = "quantum_volume_" + str([num_qubits, depth, seed]).replace(" ", "") + width = num_qubits // 2 # how many SU(4)s fit in each layer + rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed) + if seed is None: + # Get the internal entropy used to seed the default RNG, if no seed was given. This + # stays in the output name, so effectively stores a way of regenerating the circuit. + # This is just best-effort only, for backwards compatibility, and isn't critical (if + # someone needs full reproducibility, they should be manually controlling the seeding). + seed = getattr(getattr(rng.bit_generator, "seed_seq", None), "entropy", None) - # Generator random unitary seeds in advance. - # Note that this means we are constructing multiple new generator - # objects from low-entropy integer seeds rather than pass the shared - # generator object to the random_unitary function. This is done so - # that we can use the integer seed as a label for the generated gates. - unitary_seeds = rng.integers(low=1, high=1000, size=[depth, width]) + super().__init__( + num_qubits, name="quantum_volume_" + str([num_qubits, depth, seed]).replace(" ", "") + ) + base = self if flatten else QuantumCircuit(num_qubits, name=self.name) # For each layer, generate a permutation of qubits # Then generate and apply a Haar-random SU(4) to each pair - circuit = QuantumCircuit(num_qubits, name=name) - perm_0 = list(range(num_qubits)) - for d in range(depth): - perm = rng.permutation(perm_0) - if not classical_permutation: - layer_perm = Permutation(num_qubits, perm) - circuit.compose(layer_perm, inplace=True) - for w in range(width): - seed_u = unitary_seeds[d][w] - su4 = random_unitary(4, seed=seed_u).to_instruction() - su4.label = "su4_" + str(seed_u) - if classical_permutation: - physical_qubits = int(perm[2 * w]), int(perm[2 * w + 1]) - circuit.compose(su4, [physical_qubits[0], physical_qubits[1]], inplace=True) - else: - circuit.compose(su4, [2 * w, 2 * w + 1], inplace=True) - - super().__init__(*circuit.qregs, name=circuit.name) - self.compose(circuit.to_instruction(), qubits=self.qubits, inplace=True) + unitaries = scipy.stats.unitary_group.rvs(4, depth * width, rng).reshape(depth, width, 4, 4) + qubits = tuple(base.qubits) + for row in unitaries: + perm = rng.permutation(num_qubits) + if classical_permutation: + for w, unitary in enumerate(row): + gate = UnitaryGate(unitary, check_input=False, num_qubits=2) + qubit = 2 * w + base._append( + CircuitInstruction(gate, (qubits[perm[qubit]], qubits[perm[qubit + 1]])) + ) + else: + base._append(CircuitInstruction(PermutationGate(perm), qubits)) + for w, unitary in enumerate(row): + gate = UnitaryGate(unitary, check_input=False, num_qubits=2) + qubit = 2 * w + base._append(CircuitInstruction(gate, qubits[qubit : qubit + 2])) + if not flatten: + self._append(CircuitInstruction(base.to_instruction(), tuple(self.qubits))) diff --git a/releasenotes/notes/qv-perf-be76290f472e4777.yaml b/releasenotes/notes/qv-perf-be76290f472e4777.yaml new file mode 100644 index 000000000000..f7e65901eae0 --- /dev/null +++ b/releasenotes/notes/qv-perf-be76290f472e4777.yaml @@ -0,0 +1,27 @@ +--- +features_circuits: + - | + Construction time for :class:`.QuantumVolume` circuits has been significantly improved, on the + order of 10x or a bit more. The internal SU4 gates will now also use more bits of randomness + during their generation, leading to more representative volume circuits, especially at large + widths and depths. + - | + :class:`.QuantumVolume` now has a ``flatten`` keyword argument. This defaults to ``False``, + where the constructed circuit contains a single instruction that in turn contains the actual + volume structure. If set ``True``, the circuit will directly have the volumetric SU4 matrices. + - | + :class:`.UnitaryGate` now accepts an optional ``num_qubits`` argument. The only effect of this + is to skip the inference of the qubit count, which can be helpful for performance when many + gates are being constructed. +upgrade_circuits: + - | + The random-number usage of :class:`.QuantumVolume` has changed, so you will get a different + circuit for a fixed seed between older versions of Qiskit and this version. The random-unitary + generation now uses more bits of entropy, so large circuits will be less biased. + - | + The internal :class:`.UnitaryGate` instances in the definition of a :class:`.QuantumVolume` + circuit will no longer have a :attr:`~.Instruction.label` field set. Previously this was set + to the string ``su4_`` where ```` was a three-digit number denoting the seed of an + internal Numpy pRNG instance for that gate. Doing this was a serious performance problem, and + the seed ought not to have been useful; if you need to retrieve the matrix from the gate, simply + use the :meth:`.Gate.to_matrix` method. diff --git a/test/python/circuit/library/test_quantum_volume.py b/test/python/circuit/library/test_quantum_volume.py index f2d809fec399..80c762db2640 100644 --- a/test/python/circuit/library/test_quantum_volume.py +++ b/test/python/circuit/library/test_quantum_volume.py @@ -15,25 +15,25 @@ import unittest from test.utils.base import QiskitTestCase -from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import QuantumVolume -from qiskit.quantum_info import Operator -from qiskit.quantum_info.random import random_unitary class TestQuantumVolumeLibrary(QiskitTestCase): """Test library of quantum volume quantum circuits.""" - def test_qv(self): + def test_qv_seed_reproducibility(self): """Test qv circuit.""" - circuit = QuantumVolume(2, 2, seed=2, classical_permutation=False) - expected = QuantumCircuit(2) - expected.swap(0, 1) - expected.append(random_unitary(4, seed=837), [0, 1]) - expected.append(random_unitary(4, seed=262), [0, 1]) - expected = Operator(expected) - simulated = Operator(circuit) - self.assertTrue(expected.equiv(simulated)) + left = QuantumVolume(4, 4, seed=28, classical_permutation=False) + right = QuantumVolume(4, 4, seed=28, classical_permutation=False) + self.assertEqual(left, right) + + left = QuantumVolume(4, 4, seed=3, classical_permutation=True) + right = QuantumVolume(4, 4, seed=3, classical_permutation=True) + self.assertEqual(left, right) + + left = QuantumVolume(4, 4, seed=2024, flatten=True) + right = QuantumVolume(4, 4, seed=2024, flatten=True) + self.assertEqual(left, right) if __name__ == "__main__": From a7cc16624a719332b8ad5ab764425dca33f3952c Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 9 Apr 2024 11:56:53 +0200 Subject: [PATCH 5/7] Add "extended support" as a badge (#12024) * extended support * addressing comments --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 21fdc1439bf2..9bbebfe0db4a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Qiskit + [![License](https://img.shields.io/github/license/Qiskit/qiskit.svg?)](https://opensource.org/licenses/Apache-2.0) -[![Release](https://img.shields.io/github/release/Qiskit/qiskit.svg)](https://github.com/Qiskit/qiskit/releases) +[![Current Release](https://img.shields.io/github/release/Qiskit/qiskit.svg?logo=Qiskit)](https://github.com/Qiskit/qiskit/releases) +[![Extended Support Release](https://img.shields.io/github/v/release/Qiskit/qiskit?sort=semver&filter=0.*&logo=Qiskit&label=extended%20support)](https://github.com/Qiskit/qiskit/releases?q=tag%3A0) [![Downloads](https://img.shields.io/pypi/dm/qiskit.svg)](https://pypi.org/project/qiskit/) [![Coverage Status](https://coveralls.io/repos/github/Qiskit/qiskit/badge.svg?branch=main)](https://coveralls.io/github/Qiskit/qiskit?branch=main) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qiskit) From e48b4c47deb5968d013d5807bfa110360f8913d0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 9 Apr 2024 20:03:51 -0400 Subject: [PATCH 6/7] Reduce number of decomposers used during UnitarySynthesis default plugin (#12151) * Reduce number of decomposers used during UnitarySynthesis default plugin This commit reduces the number of decomposers we run in the default synthesis plugin when we're in known targets. Previously the unitary synthesis plugin was trying to product of all 1q basis and 2q basis for every 2q pair that is being synthesized. This would result in duplicated work for several 1q bases where there were potential subsets available as two different target euler bases, mainly U321 and U3 or ZSX and ZSXX if the basis gates for a qubit where U3, U2, U1 or Rz, SX, and X respectively. This reuses the logic from Optimize1qGatesDecomposition to make the euler basis selection which does the deduplication. Similarly, in the presence of known 2q gates we can skip the XXDecomposer checks (or potentially running the XXDecomposer) which should speed up both the selection of decomposers and also reduce the number of decomposers we run. * Update qiskit/transpiler/passes/synthesis/unitary_synthesis.py --------- Co-authored-by: John Lapeyre --- .../passes/synthesis/unitary_synthesis.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 8e0ce6385eac..606df60869c9 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -35,6 +35,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.synthesis.one_qubit import one_qubit_decompose +from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import _possible_decomposers from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, @@ -84,23 +85,16 @@ def _choose_kak_gate(basis_gates): def _choose_euler_basis(basis_gates): """Choose the first available 1q basis to use in the Euler decomposition.""" basis_set = set(basis_gates or []) - - for basis, gates in one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES.items(): - - if set(gates).issubset(basis_set): - return basis - + decomposers = _possible_decomposers(basis_set) + if decomposers: + return decomposers[0] return "U" def _find_matching_euler_bases(target, qubit): """Find matching available 1q basis to use in the Euler decomposition.""" - euler_basis_gates = [] basis_set = target.operation_names_for_qargs((qubit,)) - for basis, gates in one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES.items(): - if set(gates).issubset(basis_set): - euler_basis_gates.append(basis) - return euler_basis_gates + return _possible_decomposers(basis_set) def _choose_bases(basis_gates, basis_dict=None): @@ -777,6 +771,13 @@ def is_controlled(gate): ) decomposers.append(decomposer) + # If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer + # is an ideal decomposition and there is no need to bother calculating the XX embodiments + # or try the XX decomposer + if {"cx", "cz", "ecr"}.issuperset(available_2q_basis): + self._decomposer_cache[qubits_tuple] = decomposers + return decomposers + # possible controlled decomposers (i.e. XXDecomposer) controlled_basis = {k: v for k, v in available_2q_basis.items() if is_controlled(v)} basis_2q_fidelity = {} From 01e9a7c0784b64a2113028b5d02f1590df01eaec Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Wed, 10 Apr 2024 09:16:55 -0400 Subject: [PATCH 7/7] removing lint exclusion statement in favor of unused variable (#12164) --- test/python/circuit/library/test_state_preparation.py | 3 +-- test/python/circuit/test_circuit_properties.py | 3 +-- test/python/pulse/test_pulse_lib.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/python/circuit/library/test_state_preparation.py b/test/python/circuit/library/test_state_preparation.py index f4966ab61a73..adad848a2777 100644 --- a/test/python/circuit/library/test_state_preparation.py +++ b/test/python/circuit/library/test_state_preparation.py @@ -98,10 +98,9 @@ def test_incompatible_state_and_qubit_args(self): def test_incompatible_int_state_and_qubit_args(self): """Test error raised if number of qubits not compatible with integer state arg""" - # pylint: disable=pointless-statement with self.assertRaises(QiskitError): stateprep = StatePreparation(5, num_qubits=2) - stateprep.definition + _ = stateprep.definition def test_int_state_and_no_qubit_args(self): """Test automatic determination of qubit number""" diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index bce49af689af..481f2fe3ca56 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -1240,8 +1240,7 @@ def test_scheduling(self): qc.cx(0, 1) with self.assertRaises(AttributeError): - # pylint: disable=pointless-statement - qc.op_start_times + _ = qc.op_start_times if __name__ == "__main__": diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index daf163a2c32b..9c57579d0bf1 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -727,8 +727,7 @@ def test_get_parameters(self): self.assertEqual(drag_pulse.beta, 3) with self.assertRaises(AttributeError): - # pylint: disable=pointless-statement - drag_pulse.non_existing_parameter + _ = drag_pulse.non_existing_parameter def test_envelope_cache(self): """Test speed up of instantiation with lambdify envelope cache."""