From c40ce8327bf3b57b25831e3ddf8e1c54ce8e48e7 Mon Sep 17 00:00:00 2001 From: aeddins-ibm <60495383+aeddins-ibm@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:09:08 -0400 Subject: [PATCH 01/27] Fix `BitArray.from_counts`/`from_samples` to not fail for input with only `0` outcome and `num_bits=None` (#12800) * interpret `0` as represented by 1 bit * Test conversion of counts w only `0` outcome * add release note --- qiskit/primitives/containers/bit_array.py | 7 +++++-- ...itarray-fromcounts-nobits-82958a596b3489ec.yaml | 7 +++++++ .../python/primitives/containers/test_bit_array.py | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index 5c68893a4da2..11cd91a96521 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -233,7 +233,7 @@ def from_counts( Args: counts: One or more counts-like mappings with the same number of shots. num_bits: The desired number of bits per shot. If unset, the biggest value found sets - this value. + this value, with a minimum of one bit. Returns: A new bit array with shape ``()`` for single input counts, or ``(N,)`` for an iterable @@ -277,7 +277,7 @@ def from_samples( Args: samples: A list of bitstrings, a list of integers, or a list of hexstrings. num_bits: The desired number of bits per sample. If unset, the biggest sample provided - is used to determine this value. + is used to determine this value, with a minimum of one bit. Returns: A new bit array. @@ -300,6 +300,9 @@ def from_samples( # we are forced to prematurely look at every iterand in this case ints = list(ints) num_bits = max(map(int.bit_length, ints)) + # convention: if the only value is 0, represent with one bit: + if num_bits == 0: + num_bits = 1 num_bytes = _min_num_bytes(num_bits) data = b"".join(val.to_bytes(num_bytes, "big") for val in ints) diff --git a/releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml b/releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml new file mode 100644 index 000000000000..6be5ea21bdf5 --- /dev/null +++ b/releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in :meth:`.BitArray.from_counts` and :meth:`.BitArray.from_samples`. + Previously these would raise an error if given data containing only zeros, and no + value for the optional argument ``num_bits``. Now they produce a :class:`.BitArray` + with :attr:`.BitArray.num_bits` set to 1. diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index e4963d046f51..4aeeba854b33 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -232,6 +232,7 @@ def convert(counts: Counts): counts1 = convert(Counts({"0b101010": 2, "0b1": 3, "0x010203": 4})) counts2 = convert(Counts({1: 3, 2: 6})) + counts3 = convert(Counts({0: 2})) bit_array = BitArray.from_counts(counts1) expected = BitArray(u_8([[0, 0, 42]] * 2 + [[0, 0, 1]] * 3 + [[1, 2, 3]] * 4), 17) @@ -248,6 +249,10 @@ def convert(counts: Counts): ] self.assertEqual(bit_array, BitArray(u_8(expected), 17)) + bit_array = BitArray.from_counts(counts3) + expected = BitArray(u_8([[0], [0]]), 1) + self.assertEqual(bit_array, expected) + def test_from_samples_bitstring(self): """Test the from_samples static constructor.""" bit_array = BitArray.from_samples(["110", "1", "1111111111"]) @@ -256,6 +261,9 @@ def test_from_samples_bitstring(self): bit_array = BitArray.from_samples(["110", "1", "1111111111"], 20) self.assertEqual(bit_array, BitArray(u_8([[0, 0, 6], [0, 0, 1], [0, 3, 255]]), 20)) + bit_array = BitArray.from_samples(["000", "0"]) + self.assertEqual(bit_array, BitArray(u_8([[0], [0]]), 1)) + def test_from_samples_hex(self): """Test the from_samples static constructor.""" bit_array = BitArray.from_samples(["0x01", "0x0a12", "0x0105"]) @@ -264,6 +272,9 @@ def test_from_samples_hex(self): bit_array = BitArray.from_samples(["0x01", "0x0a12", "0x0105"], 20) self.assertEqual(bit_array, BitArray(u_8([[0, 0, 1], [0, 10, 18], [0, 1, 5]]), 20)) + bit_array = BitArray.from_samples(["0x0", "0x0"]) + self.assertEqual(bit_array, BitArray(u_8([[0], [0]]), 1)) + def test_from_samples_int(self): """Test the from_samples static constructor.""" bit_array = BitArray.from_samples([1, 2578, 261]) @@ -272,6 +283,9 @@ def test_from_samples_int(self): bit_array = BitArray.from_samples([1, 2578, 261], 20) self.assertEqual(bit_array, BitArray(u_8([[0, 0, 1], [0, 10, 18], [0, 1, 5]]), 20)) + bit_array = BitArray.from_samples([0, 0, 0]) + self.assertEqual(bit_array, BitArray(u_8([[0], [0], [0]]), 1)) + def test_reshape(self): """Test the reshape method.""" # this creates incrementing bitstrings from 0 to 360 * 32 - 1 From 4ec51d1a83987e022eb7323590b6d7b218ede28d Mon Sep 17 00:00:00 2001 From: YingHongTham Date: Fri, 26 Jul 2024 14:25:13 +0200 Subject: [PATCH 02/27] fixes #9355 deprecate visualize_transition (#12287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added deprecation warnings and decorators to transition_visualization.py * fixed style with tox -epy310 -eblack * added release notes * added to warning category=DeprecationWarning and stacklevel=2 * Update releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml Co-authored-by: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> * Update releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml Co-authored-by: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> * Apply suggestions from code review * Fix lint complaint --------- Co-authored-by: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> Co-authored-by: Matthew Treinish Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/visualization/transition_visualization.py | 5 +++++ .../deprecate-visualize_transition-8c1d257b7f37aa58.yaml | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml diff --git a/qiskit/visualization/transition_visualization.py b/qiskit/visualization/transition_visualization.py index a2ff74799999..0d1623831883 100644 --- a/qiskit/visualization/transition_visualization.py +++ b/qiskit/visualization/transition_visualization.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils.deprecation import deprecate_func def _normalize(v, tolerance=0.00001): @@ -121,6 +122,10 @@ def vector_norm(self): return np.linalg.norm(v) +@deprecate_func( + since="1.2.0", + removal_timeline="in the 2.0 release", +) def visualize_transition(circuit, trace=False, saveas=None, fpg=100, spg=2): """ Creates animation showing transitions between states of a single diff --git a/releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml b/releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml new file mode 100644 index 000000000000..6900dcbd7676 --- /dev/null +++ b/releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml @@ -0,0 +1,7 @@ +--- +deprecations_visualization: + - | + The :func:`.transition_visualization` function has been deprecated and will + be removed in the 2.0.0 release. This function had a number of limitations + which limited it's utility to only very specific use cases and didn't fit in with + the rest of the Qiskit visualization module. From edf0b53a01b708f3ead6f1bdc50becac28bf59f1 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:27:21 +0200 Subject: [PATCH 03/27] Setting noise_info to false in GenericBackendV2 creates issues later in the transpiler pipeline (#12769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * up * fix * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * lint --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../fake_provider/generic_backend_v2.py | 4 +- .../fake_provider/test_generic_backend_v2.py | 61 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 09827eba48b7..c3de76cbd0e6 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -415,7 +415,9 @@ def _build_generic_target(self): noise_params = self._get_noise_defaults(name, gate.num_qubits) self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) else: - self._target.add_instruction(gate, properties=None, name=name) + qarg_set = self._coupling_map if gate.num_qubits > 1 else range(self.num_qubits) + props = {(qarg,) if isinstance(qarg, int) else qarg: None for qarg in qarg_set} + self._target.add_instruction(gate, properties=props, name=name) if self._control_flow: self._target.add_instruction(IfElseOp, name="if_else") diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index 33bf57cf3903..70ac50f659c9 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -16,8 +16,10 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.quantum_info import Operator from qiskit.transpiler import CouplingMap from qiskit.exceptions import QiskitError +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -57,12 +59,67 @@ def test_calibration_no_noise_info(self): def test_no_noise(self): """Test no noise info when parameter is false""" - backend = GenericBackendV2(num_qubits=2, noise_info=False) + backend = GenericBackendV2( + num_qubits=5, coupling_map=CouplingMap.from_line(5), noise_info=False + ) + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 4) + qc.cx(3, 0) + qc.cx(2, 4) + qc_res = generate_preset_pass_manager(optimization_level=2, backend=backend).run(qc) + self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) + self.assertEqual(backend.target.qubit_properties, None) + + def test_no_noise_fully_connected(self): + """Test no noise info when parameter is false""" + backend = GenericBackendV2(num_qubits=5, noise_info=False) + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 4) + qc.cx(3, 0) + qc.cx(2, 4) + qc_res = generate_preset_pass_manager(optimization_level=2, backend=backend).run(qc) + self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) + self.assertEqual(backend.target.qubit_properties, None) + + def test_no_info(self): + """Test no noise info when parameter is false""" + backend = GenericBackendV2( + num_qubits=5, + coupling_map=CouplingMap.from_line(5), + noise_info=False, + pulse_channels=False, + ) + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 4) + qc.cx(3, 0) + qc.cx(2, 4) + qc_res = generate_preset_pass_manager(optimization_level=2, backend=backend).run(qc) + self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) self.assertEqual(backend.target.qubit_properties, None) def test_no_pulse_channels(self): """Test no/empty pulse channels when parameter is false""" - backend = GenericBackendV2(num_qubits=2, pulse_channels=False) + backend = GenericBackendV2( + num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False + ) + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 4) + qc.cx(3, 0) + qc.cx(2, 4) + qc_res = generate_preset_pass_manager(optimization_level=2, backend=backend).run(qc) + self.assertTrue(Operator.from_circuit(qc_res).equiv(qc)) self.assertTrue(len(backend.channels_map) == 0) def test_operation_names(self): From 1512535a973a1418f9891674f6b3779123a052f1 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 26 Jul 2024 17:55:50 +0200 Subject: [PATCH 04/27] Add `AnnotatedOperation.params` and fix some control issues (#12752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * AnnotatedOp.params support and Gate.control fix * add reno * lint * update reno * review comments - use attribute error - more clearly state the new None arg in reno and Gate class * review from Elena * Fix ``AttributeError`` test * lint * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/circuit/annotated_operation.py | 22 ++++++ qiskit/circuit/gate.py | 15 ++-- .../library/generalized_gates/unitary.py | 6 +- qiskit/circuit/library/standard_gates/h.py | 13 ++-- .../multi_control_rotation_gates.py | 15 +++- qiskit/circuit/library/standard_gates/p.py | 18 ++--- qiskit/circuit/library/standard_gates/rx.py | 22 ++++-- qiskit/circuit/library/standard_gates/rxx.py | 38 ++++++++++- qiskit/circuit/library/standard_gates/ry.py | 22 ++++-- qiskit/circuit/library/standard_gates/ryy.py | 38 ++++++++++- qiskit/circuit/library/standard_gates/rz.py | 23 +++++-- qiskit/circuit/library/standard_gates/rzx.py | 38 ++++++++++- qiskit/circuit/library/standard_gates/rzz.py | 38 ++++++++++- qiskit/circuit/library/standard_gates/s.py | 68 +++++++++++++++++++ qiskit/circuit/library/standard_gates/swap.py | 12 ++-- qiskit/circuit/library/standard_gates/sx.py | 12 ++-- qiskit/circuit/library/standard_gates/u.py | 22 ++++-- qiskit/circuit/library/standard_gates/u1.py | 6 +- qiskit/circuit/library/standard_gates/u3.py | 22 ++++-- qiskit/circuit/library/standard_gates/x.py | 12 ++-- .../library/standard_gates/xx_minus_yy.py | 38 ++++++++++- .../library/standard_gates/xx_plus_yy.py | 38 ++++++++++- qiskit/circuit/library/standard_gates/y.py | 2 +- qiskit/circuit/library/standard_gates/z.py | 2 +- qiskit/circuit/quantumcircuit.py | 2 +- .../annotated-params-116288d5628f7ee8.yaml | 30 ++++++++ .../circuit/test_annotated_operation.py | 49 ++++++++++++- test/python/circuit/test_controlled_gate.py | 54 +++++++++++++-- 28 files changed, 585 insertions(+), 92 deletions(-) create mode 100644 releasenotes/notes/annotated-params-116288d5628f7ee8.yaml diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 6780cc2e330f..6006e68f58df 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -18,6 +18,7 @@ from typing import Union, List from qiskit.circuit.operation import Operation +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int from qiskit.circuit.exceptions import CircuitError @@ -219,6 +220,27 @@ def power(self, exponent: float, annotated: bool = False): extended_modifiers.append(PowerModifier(exponent)) return AnnotatedOperation(self.base_op, extended_modifiers) + @property + def params(self) -> list[ParameterValueType]: + """The params of the underlying base operation.""" + return getattr(self.base_op, "params", []) + + @params.setter + def params(self, value: list[ParameterValueType]): + if hasattr(self.base_op, "params"): + self.base_op.params = value + else: + raise AttributeError( + f"Cannot set attribute ``params`` on the base operation {self.base_op}." + ) + + def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueType: + """Validate a parameter for the underlying base operation.""" + if hasattr(self.base_op, "validate_parameter"): + return self.base_op.validate_parameter(parameter) + + raise AttributeError(f"Cannot validate parameters on the base operation {self.base_op}.") + def _canonicalize_modifiers(modifiers): """ diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index d2c88f40bdb6..37fd19e2022a 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -104,10 +104,9 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: int | str | None = None, - annotated: bool = False, + annotated: bool | None = None, ): - """ - Return the controlled version of itself. + """Return the controlled version of itself. Implemented either as a controlled gate (ref. :class:`.ControlledGate`) or as an annotated operation (ref. :class:`.AnnotatedOperation`). @@ -118,8 +117,12 @@ def control( operation. ctrl_state: the control state in decimal or as a bitstring (e.g. ``'111'``). If ``None``, use ``2**num_ctrl_qubits-1``. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate is implemented + as an annotated gate. If ``None``, this is set to ``False`` + if the controlled gate can directly be constructed, and otherwise + set to ``True``. This allows defering the construction process in case the + synthesis of the controlled gate requires more information (e.g. + values of unbound parameters). Returns: Controlled version of the given operation. @@ -127,7 +130,7 @@ def control( Raises: QiskitError: unrecognized mode or invalid ctrl_state """ - if not annotated: + if not annotated: # captures both None and False # pylint: disable=cyclic-import from .add_control import add_control diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 6a6623ffce5d..9aa03be86318 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -165,7 +165,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: int | str | None = None, - annotated: bool = False, + annotated: bool | None = None, ) -> ControlledGate | AnnotatedOperation: """Return controlled version of gate. @@ -174,8 +174,8 @@ def control( label: Optional gate label. ctrl_state: The control state in decimal or as a bit string (e.g. ``"1011"``). If ``None``, use ``2**num_ctrl_qubits - 1``. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: Controlled version of gate. diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index c07895ebbeaa..462ede2c93ae 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Hadamard gate.""" + +from __future__ import annotations + from math import sqrt, pi from typing import Optional, Union import numpy @@ -79,9 +82,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[int, str]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-H gate. @@ -92,8 +95,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py index 98dedc05f5ef..6e31c99005b3 100644 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py @@ -18,7 +18,7 @@ from typing import Optional, Union, Tuple, List import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit +from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, ParameterExpression from qiskit.circuit.library.standard_gates.x import MCXGate from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code from qiskit.circuit.parameterexpression import ParameterValueType @@ -258,6 +258,9 @@ def mcrx( use_basis_gates=use_basis_gates, ) else: + if isinstance(theta, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRX with unbound parameter: {theta}.") + cgate = _mcsu2_real_diagonal( RXGate(theta).to_matrix(), num_controls=len(control_qubits), @@ -272,8 +275,8 @@ def mcry( q_controls: Union[QuantumRegister, List[Qubit]], q_target: Qubit, q_ancillae: Optional[Union[QuantumRegister, Tuple[QuantumRegister, int]]] = None, - mode: str = None, - use_basis_gates=False, + mode: Optional[str] = None, + use_basis_gates: bool = False, ): """ Apply Multiple-Controlled Y rotation gate @@ -333,6 +336,9 @@ def mcry( use_basis_gates=use_basis_gates, ) else: + if isinstance(theta, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRY with unbound parameter: {theta}.") + cgate = _mcsu2_real_diagonal( RYGate(theta).to_matrix(), num_controls=len(control_qubits), @@ -383,6 +389,9 @@ def mcrz( else: self.append(CRZGate(lam), control_qubits + [target_qubit]) else: + if isinstance(lam, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRZ with unbound parameter: {lam}.") + cgate = _mcsu2_real_diagonal( RZGate(lam).to_matrix(), num_controls=len(control_qubits), diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 8c83aa464027..cb2c19bf51e9 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -99,7 +99,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Return a (multi-)controlled-Phase gate. @@ -108,8 +108,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. @@ -255,7 +255,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Controlled version of this gate. @@ -264,8 +264,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. @@ -396,7 +396,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Controlled version of this gate. @@ -405,8 +405,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index cb851a740d28..4b8c9e6b446a 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -12,6 +12,8 @@ """Rotation around the X axis.""" +from __future__ import annotations + import math from math import pi from typing import Optional, Union @@ -20,7 +22,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -78,9 +80,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RX gate. @@ -89,16 +91,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRXGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index 1c06ae05a85b..3b069aa933bb 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Two-qubit XX-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional import numpy from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -111,6 +114,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RXX gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RXX gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index b60b34ffde6f..614d4ef13a0f 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -12,6 +12,8 @@ """Rotation around the Y axis.""" +from __future__ import annotations + import math from math import pi from typing import Optional, Union @@ -19,7 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -77,9 +79,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RY gate. @@ -88,16 +90,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRYGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 91d7d8096cf9..ad185e88d04b 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Two-qubit YY-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional import numpy as np from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -111,6 +114,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-YY gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RYY gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 78cf20efa5c6..3abef37b7534 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Rotation around the Z axis.""" + +from __future__ import annotations + from cmath import exp from typing import Optional, Union from qiskit.circuit.gate import Gate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -88,9 +91,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RZ gate. @@ -99,16 +102,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRZGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 90e7b71c0a33..003805cc6b55 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -11,11 +11,14 @@ # that they have been altered from the originals. """Two-qubit ZX-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -155,6 +158,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RZX gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RZX gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 554ad4954a31..ca3e6d2db2da 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -11,11 +11,14 @@ # that they have been altered from the originals. """Two-qubit ZZ-rotation gate.""" + +from __future__ import annotations + from cmath import exp from typing import Optional from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -119,6 +122,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RZZ gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RZZ gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 975d1cb3be8c..e859de4b5013 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -12,6 +12,8 @@ """The S, Sdg, CS and CSdg gates.""" +from __future__ import annotations + from math import pi from typing import Optional, Union @@ -83,6 +85,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-S gate. + + One control qubit returns a :class:`.CSGate`. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. + + Returns: + ControlledGate: controlled version of this gate. + """ + if not annotated and num_ctrl_qubits == 1: + gate = CSGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) + else: + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse of S (SdgGate). @@ -162,6 +197,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-Sdg gate. + + One control qubit returns a :class:`.CSdgGate`. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. + + Returns: + ControlledGate: controlled version of this gate. + """ + if not annotated and num_ctrl_qubits == 1: + gate = CSdgGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) + else: + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse of Sdg (SGate). diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 5d33bc74b8d0..84ef3046746d 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -12,6 +12,8 @@ """Swap gate.""" +from __future__ import annotations + from typing import Optional, Union import numpy from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key @@ -90,9 +92,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-SWAP gate. @@ -103,8 +105,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index ec3c87653148..ec1f57a83bd3 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -12,6 +12,8 @@ """Sqrt(X) and C-Sqrt(X) gates.""" +from __future__ import annotations + from math import pi from typing import Optional, Union from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key @@ -104,9 +106,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-SX gate. @@ -117,8 +119,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: SingletonControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 7f1d32eb914c..bed454897929 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-pulse single-qubit gate.""" + +from __future__ import annotations + import cmath import copy as _copy import math @@ -19,7 +22,7 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -103,9 +106,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-U gate. @@ -114,8 +117,10 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. @@ -131,6 +136,11 @@ def control( ) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index e62a132670ff..e9bbed871d1f 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -128,7 +128,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -278,7 +278,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -410,7 +410,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 80581bf55a5d..df229af7d819 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -11,13 +11,16 @@ # that they have been altered from the originals. """Two-pulse single-qubit gate.""" + +from __future__ import annotations + import math from cmath import exp from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -115,9 +118,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-U3 gate. @@ -126,8 +129,10 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. @@ -136,6 +141,11 @@ def control( gate = CU3Gate(*self.params, label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 3688d376538a..f3f7b5ebdb72 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -112,7 +112,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -257,7 +257,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -453,7 +453,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -801,7 +801,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -1047,7 +1047,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -1222,7 +1222,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index db3c3dc89153..2fac02fd154d 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-qubit XX-YY gate.""" + +from __future__ import annotations + import math from cmath import exp from math import pi @@ -24,7 +27,7 @@ from qiskit.circuit.library.standard_gates.s import SdgGate, SGate from qiskit.circuit.library.standard_gates.sx import SXdgGate, SXGate from qiskit.circuit.library.standard_gates.x import CXGate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -156,6 +159,39 @@ def _define(self): self.definition = circuit + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-(XX-YY) gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Inverse gate. diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index 7920454d0b98..e0528a1f1792 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-qubit XX+YY gate.""" + +from __future__ import annotations + import math from cmath import exp from math import pi @@ -20,7 +23,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -160,6 +163,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-(XX+YY) gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse XX+YY gate (i.e. with the negative rotation angle and same phase angle). diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index d62586aa2b9b..99d37ee08bd3 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -108,7 +108,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 4b2364178a94..dd83c3833d62 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -112,7 +112,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index be8b66b38757..3b2da762c51f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1717,7 +1717,7 @@ def control( label (str): An optional label to give the controlled operation for visualization. ctrl_state (str or int): The control state in decimal or as a bitstring (e.g. '111'). If None, use ``2**num_ctrl_qubits - 1``. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml b/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml new file mode 100644 index 000000000000..2f316a06ae8d --- /dev/null +++ b/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml @@ -0,0 +1,30 @@ +--- +features_circuits: + - | + Added support for :meth:`.AnnotatedOperation.params` and + :meth:`.AnnotatedOperation.validate_parameter`, which enable + circuit-level parameter handling (such as binding parameters) for + annotated operations. +fixes: + - | + Fixed a series of issues when controlling parameterized standard gates. + The controlled version of some gates (e.g. :class:`.RXXGate` or + :class:`.RYGate` for more than 1 control) cannot be synthesized if + they contain unbound parameters. Previously, calling ``.control()`` but + now we create an :class:`.AnnotatedOperation` as placeholder. This + allows to insert the controlled gate into a circuit, bind the parameters + at a later stage, and then synthesize the operation. + Fixes `#10311 `_, + `#10697 `_, + and `#12135 `_. + - | + The :class:`.SGate` and :class:`.SdgGate` now correctly return a + :class:`.CSGate`, resp. :class:`.CSdgGate`, if they are controlled on + a single control qubit. +upgrade_circuits: + - | + The ``annotated`` argument of the :meth:`.Gate.control` method is now + ``None``, which allows Qiskit to choose whether to annotate a controlled operation. + If the concrete implementation (``annotated=False``) is available, it will be returned by + default. Otherwise, the annotated implementation will be returned (``annotated=True``). + This allows, for example, to defer the synthesis of controlled, parameterized gates. diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index f4228fc0485f..e2ca9f4af9a4 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -14,6 +14,7 @@ import unittest +from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit._utils import _compute_control_matrix from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -22,7 +23,7 @@ PowerModifier, _canonicalize_modifiers, ) -from qiskit.circuit.library import SGate, SdgGate +from qiskit.circuit.library import SGate, SdgGate, UGate, RXGate from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -157,6 +158,52 @@ def test_canonicalize_inverse(self): expected_list = [] self.assertEqual(canonical_list, expected_list) + def test_params_access(self): + """Test access to the params field.""" + p, q = Parameter("p"), Parameter("q") + params = [0.2, -1, p] + gate = UGate(*params) + annotated = gate.control(10, annotated=True) + + with self.subTest(msg="reading params"): + self.assertListEqual(annotated.params, params) + + new_params = [q, 131, -1.2] + with self.subTest(msg="setting params"): + annotated.params = new_params + self.assertListEqual(annotated.params, new_params) + + def test_binding_annotated_gate(self): + """Test binding an annotated gate in a circuit.""" + p = Parameter("p") + annotated = RXGate(p).control(2, annotated=True) + circuit = QuantumCircuit(annotated.num_qubits) + circuit.h(circuit.qubits) + circuit.append(annotated, circuit.qubits) + + with self.subTest(msg="test parameter is reported"): + self.assertEqual(circuit.num_parameters, 1) + + with self.subTest(msg="test binding parameters worked"): + bound = circuit.assign_parameters([0.321]) + self.assertEqual(bound.num_parameters, 0) + + def test_invalid_params_access(self): + """Test params access to a operation not providing params.""" + op = Operator(SGate()) + annotated = AnnotatedOperation(op, InverseModifier()) + + with self.subTest(msg="accessing params returns an empty list"): + self.assertEqual(len(annotated.params), 0) + + with self.subTest(msg="setting params fails"): + with self.assertRaises(AttributeError): + annotated.params = [1.2] + + with self.subTest(msg="validating params fails"): + with self.assertRaises(AttributeError): + _ = annotated.validate_parameter(1.2) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 6d7b237915fa..707f9d32cb94 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -19,7 +19,7 @@ from numpy import pi from ddt import ddt, data, unpack -from qiskit import QuantumRegister, QuantumCircuit, QiskitError +from qiskit import QuantumRegister, QuantumCircuit, QiskitError, transpile from qiskit.circuit import ControlledGate, Parameter, Gate from qiskit.circuit.annotated_operation import AnnotatedOperation from qiskit.circuit.singleton import SingletonControlledGate, _SingletonControlledGateOverrides @@ -46,9 +46,13 @@ CCXGate, HGate, RZGate, + RYGate, RXGate, + RZZGate, + RZXGate, + RYYGate, + RXXGate, CPhaseGate, - RYGate, CRYGate, CRXGate, CSwapGate, @@ -73,6 +77,8 @@ C3SXGate, C4XGate, MCPhaseGate, + XXMinusYYGate, + XXPlusYYGate, GlobalPhaseGate, UnitaryGate, ) @@ -761,7 +767,6 @@ def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits): yields the expected number of cx gates.""" qc = QuantumCircuit(num_ctrl_qubits + 1) qc.append(MCXGate(num_ctrl_qubits), range(num_ctrl_qubits + 1)) - from qiskit import transpile cqc = transpile(qc, basis_gates=["u", "cx"]) cx_count = cqc.count_ops()["cx"] @@ -808,8 +813,6 @@ def test_mcx_gates(self, num_ctrl_qubits): def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits): """Test if cx count of the v-chain mcx with dirty ancilla is less than upper bound.""" - from qiskit import transpile - mcx_vchain = MCXVChain(num_ctrl_qubits, dirty_ancillas=True) qc = QuantumCircuit(mcx_vchain.num_qubits) @@ -824,8 +827,6 @@ def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits): def test_mcxrecursive_clean_ancilla_cx_count(self, num_ctrl_qubits): """Test if cx count of the mcx with one clean ancilla is less than upper bound.""" - from qiskit import transpile - mcx_recursive = MCXRecursive(num_ctrl_qubits) qc = QuantumCircuit(mcx_recursive.num_qubits) @@ -1491,6 +1492,45 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): target.flat[-1] = -1 self.assertEqual(Operator(controlled), Operator(target)) + @data( + RXGate, + RYGate, + RZGate, + RXXGate, + RYYGate, + RZXGate, + RZZGate, + UGate, + U3Gate, + XXMinusYYGate, + XXPlusYYGate, + ) + def test_mc_failure_without_annotation(self, gate_cls): + """Test error for gates that cannot be multi-controlled without annotation.""" + theta = Parameter("theta") + num_params = len(_get_free_params(gate_cls.__init__, ignore=["self"])) + params = [theta] + (num_params - 1) * [1.234] + + for annotated in [False, None]: + with self.subTest(annotated=annotated): + # if annotated is False, check that a sensible error is raised + if annotated is False: + with self.assertRaisesRegex(QiskitError, "unbound parameter"): + _ = gate_cls(*params).control(5, annotated=False) + + # else, check that the gate can be synthesized after all parameters + # have been bound + else: + mc_gate = gate_cls(*params).control(5) + + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) + + bound = circuit.assign_parameters([0.5123]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + + self.assertEqual(unrolled.num_parameters, 0) + def assertEqualTranslated(self, circuit, unrolled_reference, basis): """Assert that the circuit is equal to the unrolled reference circuit.""" unroller = UnrollCustomDefinitions(std_eqlib, basis) From c8c53cc6f81f43f628f1c64ddf67f27a9d1fdb4c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 26 Jul 2024 13:29:21 -0400 Subject: [PATCH 05/27] Add a default optimization level to generate_preset_pass_manager (#12150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a default optimization level to generate_preset_pass_manager This commit adds a default value to the generate_preset_pass_manager's optimization_level argument. If it's not specified optimization level 2 will be used. After #12148 optimization level 2 is a better fit for an optimal tradeoff between heuristic effort and runtime that makes it well suited as a default optimization level. * Update transpile()'s default opt level to match This commit updates the transpile() function's optimization_level argument default value to match generate_preset_pass_manager's new default to use 2 instead of 1. This is arguably a breaking API change, but since the semantics are equivalent with two minor edge cases with implicit behavior that were a side effect of the level 1 preset pass manager's construction (which are documented in the release notes) we're ok making it in this case. Some tests which we're relying on the implicit behavior of optimization level 1 are updated to explicitly set the optimization level argument which will retain this behavior. * Update more tests expecting optimization level 1 * * Set optimization level to 1 in test_approximation_degree. * Replace use of transpile with specific pass in HLS tests. * Set optimization_level=1 in layout-dependent tests. * Expand upgrade note explanation on benefits of level 2 * Apply Elena's reno suggestions --------- Co-authored-by: Elena Peña Tapia Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/compiler/transpiler.py | 4 +- .../generate_preset_pass_manager.py | 20 +++++++-- ...e-preset-passmanager-ec758ddc896ae2d6.yaml | 44 +++++++++++++++++++ test/python/circuit/library/test_qft.py | 2 +- test/python/compiler/test_transpiler.py | 21 +++++++-- .../primitives/test_backend_estimator.py | 6 ++- test/python/primitives/test_primitive.py | 2 +- test/python/providers/test_backend_v2.py | 8 ++-- test/python/pulse/test_builder.py | 8 ++-- .../transpiler/test_basis_translator.py | 1 + .../transpiler/test_high_level_synthesis.py | 12 ++--- .../transpiler/test_preset_passmanagers.py | 18 ++++++++ test/python/transpiler/test_sabre_layout.py | 5 ++- 13 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 9c74d10a7b63..adf60ca91e56 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -218,7 +218,7 @@ def transpile( # pylint: disable=too-many-return-statements * 2: heavy optimization * 3: even heavier optimization - If ``None``, level 1 will be chosen as default. + If ``None``, level 2 will be chosen as default. callback: A callback function that will be called after each pass execution. The function will be called with 5 keyword arguments, @@ -312,7 +312,7 @@ def callback_func(**kwargs): if optimization_level is None: # Take optimization level from the configuration or 1 as default. config = user_config.get_config() - optimization_level = config.get("transpile_optimization_level", 1) + optimization_level = config.get("transpile_optimization_level", 2) if backend is not None and getattr(backend, "version", 0) <= 1: # This is a temporary conversion step to allow for a smoother transition diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 6023f2a4b6aa..bdbac42c8055 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -19,6 +19,7 @@ from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping from qiskit.circuit.quantumregister import Qubit +from qiskit.providers.backend import Backend from qiskit.providers.backend_compat import BackendV2Converter from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.exceptions import TranspilerError @@ -35,7 +36,7 @@ def generate_preset_pass_manager( - optimization_level, + optimization_level=2, backend=None, target=None, basis_gates=None, @@ -96,9 +97,10 @@ def generate_preset_pass_manager( Args: optimization_level (int): The optimization level to generate a - :class:`~.PassManager` for. This can be 0, 1, 2, or 3. Higher - levels generate more optimized circuits, at the expense of - longer transpilation time: + :class:`~.StagedPassManager` for. By default optimization level 2 + is used if this is not specified. This can be 0, 1, 2, or 3. Higher + levels generate potentially more optimized circuits, at the expense + of longer transpilation time: * 0: no optimization * 1: light optimization @@ -238,6 +240,16 @@ def generate_preset_pass_manager( ValueError: if an invalid value for ``optimization_level`` is passed in. """ + # Handle positional arguments for target and backend. This enables the usage + # pattern `generate_preset_pass_manager(backend.target)` to generate a default + # pass manager for a given target. + if isinstance(optimization_level, Target): + target = optimization_level + optimization_level = 2 + elif isinstance(optimization_level, Backend): + backend = optimization_level + optimization_level = 2 + if backend is not None and getattr(backend, "version", 0) <= 1: # This is a temporary conversion step to allow for a smoother transition # to a fully target-based transpiler pipeline while maintaining the behavior diff --git a/releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml b/releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml new file mode 100644 index 000000000000..ff5d57797860 --- /dev/null +++ b/releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml @@ -0,0 +1,44 @@ +--- +features_transpiler: + - | + The ``optimization_level`` argument for the :func:`.generate_preset_pass_manager` function is + now optional. If it's not specified it will default to using optimization level 2. As the argument + is now optional, the first positional argument has been expanded to enable passing a :class:`.Target` + or a :class:`.BackendV2` as the first argument for more convenient construction. For example:: + + from qiskit.transpiler.preset_passmanager import generate_preset_pass_manager + from qiskit.providers.fake_provider import GenericBackendV2 + + backend = GenericBackendV2(100) + + generate_preset_pass_manager(backend.Target) + + will construct a default pass manager for the 100 qubit :class`.GenericBackendV2` instance. +upgrade_transpiler: + - | + The default ``optimization_level`` used by the :func:`.transpile` function when one is not + specified has been changed to level 2. This makes it consistent with the default used + by :func:`.generate_preset_pass_manager` which is used internally by :func:`.transpile`. Optimization + level 2 provides a much better balance between the run time of the function and the optimizations it + performs, it's a better tradeoff to use by default. + + The API of :func:`.transpile` remains unchanged because, fundamentally, level 2 and level 1 + have the same semantics. If you were previously relying on the implicit default of level 1, + you can simply set the argument ``optimization_level=1`` when you call :func:`.transpile`. + Similarly you can change the default back in your local environment by using a user config + file and setting the ``transpile_optimization_level`` field to 1. + + The only potential issue is that your transpilation workflow may be relying on an implicit trivial layout (where qubit 0 + in the circuit passed to :func:`.transpile` is mapped to qubit 0 on the target backend/coupling, + 1->1, 2->2, etc.) without specifying ``optimization_level=1``, ``layout_method="trivial"``, or + explicitly setting ``initial_layout`` when calling :func:`.transpile`. This behavior was a side + effect of the preset pass manager construction in optimization level 1 and is not mirrored in + level 2. If you need this behavior you can use any of the three options listed previously to make + this behavior explicit. + + Similarly, if you were targeting a discrete basis gate set you may encounter an issue using the + new default with optimization level 2 (or running explicitly optimization level 3), as the additional optimization passes that run in + level 2 and 3 don't work in all cases with a discrete basis. You can explicitly set + ``optimization_level=1`` manually in this case. In general the transpiler does not currently + fully support discrete basis sets and if you're relying on this you should likely construct a + pass manager manually to build a compilation pipeline that will work with your target. diff --git a/test/python/circuit/library/test_qft.py b/test/python/circuit/library/test_qft.py index 1f5c9715dd7a..85837f0ac80c 100644 --- a/test/python/circuit/library/test_qft.py +++ b/test/python/circuit/library/test_qft.py @@ -139,7 +139,7 @@ def test_qft_num_gates(self, num_qubits, approximation_degree, insert_barriers): qft = QFT( num_qubits, approximation_degree=approximation_degree, insert_barriers=insert_barriers ) - ops = transpile(qft, basis_gates=basis_gates).count_ops() + ops = transpile(qft, basis_gates=basis_gates, optimization_level=1).count_ops() with self.subTest(msg="assert H count"): self.assertEqual(ops["h"], num_qubits) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 77b63a3098bc..472fc732ca9a 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -517,11 +517,21 @@ def test_transpile_bell_discrete_basis(self): # Try with the initial layout in both directions to ensure we're dealing with the basis # having only a single direction. + + # Use optimization level=1 because the synthesis that runs as part of optimization at + # higher optimization levels will create intermediate gates that the transpiler currently + # lacks logic to translate to a discrete basis. self.assertIsInstance( - transpile(qc, target=target, initial_layout=[0, 1], seed_transpiler=42), QuantumCircuit + transpile( + qc, target=target, initial_layout=[0, 1], seed_transpiler=42, optimization_level=1 + ), + QuantumCircuit, ) self.assertIsInstance( - transpile(qc, target=target, initial_layout=[1, 0], seed_transpiler=42), QuantumCircuit + transpile( + qc, target=target, initial_layout=[1, 0], seed_transpiler=42, optimization_level=1 + ), + QuantumCircuit, ) def test_transpile_one(self): @@ -1318,6 +1328,7 @@ def test_transpile_calibrated_custom_gate_on_diff_qubit(self): backend=GenericBackendV2(num_qubits=4), layout_method="trivial", seed_transpiler=42, + optimization_level=1, ) def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self): @@ -1334,7 +1345,7 @@ def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self): circ.add_calibration("h", [1], q0_x180) transpiled_circuit = transpile( - circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42 + circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42, optimization_level=1 ) self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) self.assertEqual(set(transpiled_circuit.count_ops().keys()), {"rz", "sx", "h"}) @@ -1781,7 +1792,7 @@ def test_approximation_degree_invalid(self): ) def test_approximation_degree(self): - """Test more approximation gives lower-cost circuit.""" + """Test more approximation can give lower-cost circuit.""" circuit = QuantumCircuit(2) circuit.swap(0, 1) circuit.h(0) @@ -1791,6 +1802,7 @@ def test_approximation_degree(self): translation_method="synthesis", approximation_degree=0.1, seed_transpiler=42, + optimization_level=1, ) circ_90 = transpile( circuit, @@ -1798,6 +1810,7 @@ def test_approximation_degree(self): translation_method="synthesis", approximation_degree=0.9, seed_transpiler=42, + optimization_level=1, ) self.assertLess(circ_10.depth(), circ_90.depth()) diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index c3deb0735ca1..626ba7625bc9 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -430,7 +430,7 @@ def test_layout(self, backend): backend.set_options(seed_simulator=15) with self.assertWarns(DeprecationWarning): estimator = BackendEstimator(backend) - estimator.set_transpile_options(seed_transpiler=15) + estimator.set_transpile_options(seed_transpiler=15, optimization_level=1) value = estimator.run(qc, op, shots=10000).result().values[0] if optionals.HAS_AER: ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.916 @@ -446,7 +446,9 @@ def test_layout(self, backend): op = SparsePauliOp("IZI") with self.assertWarns(DeprecationWarning): estimator = BackendEstimator(backend) - estimator.set_transpile_options(initial_layout=[0, 1, 2], seed_transpiler=15) + estimator.set_transpile_options( + initial_layout=[0, 1, 2], seed_transpiler=15, optimization_level=1 + ) estimator.set_options(seed_simulator=15) value = estimator.run(qc, op, shots=10000).result().values[0] if optionals.HAS_AER: diff --git a/test/python/primitives/test_primitive.py b/test/python/primitives/test_primitive.py index f0401e771448..c2b6b8f14202 100644 --- a/test/python/primitives/test_primitive.py +++ b/test/python/primitives/test_primitive.py @@ -142,7 +142,7 @@ def test_with_scheduling(n): qc = QuantumCircuit(1) qc.x(0) qc.add_calibration("x", qubits=(0,), schedule=custom_gate) - return transpile(qc, Fake20QV1(), scheduling_method="alap") + return transpile(qc, Fake20QV1(), scheduling_method="alap", optimization_level=1) keys = [_circuit_key(test_with_scheduling(i)) for i in range(1, 5)] self.assertEqual(len(keys), len(set(keys))) diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 70330085b1ab..40924e240826 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -147,7 +147,7 @@ def test_transpile_respects_arg_constraints(self): qc = QuantumCircuit(2) qc.h(0) qc.cx(1, 0) - tqc = transpile(qc, self.backend) + tqc = transpile(qc, self.backend, optimization_level=1) self.assertTrue(Operator.from_circuit(tqc).equiv(qc)) # Below is done to check we're decomposing cx(1, 0) with extra # rotations to correct for direction. However because of fp @@ -163,7 +163,7 @@ def test_transpile_respects_arg_constraints(self): qc = QuantumCircuit(2) qc.h(0) qc.ecr(0, 1) - tqc = transpile(qc, self.backend) + tqc = transpile(qc, self.backend, optimization_level=1) self.assertTrue(Operator.from_circuit(tqc).equiv(qc)) self.assertEqual(tqc.count_ops(), {"ecr": 1, "u": 4}) self.assertMatchesTargetConstraints(tqc, self.backend.target) @@ -173,7 +173,7 @@ def test_transpile_relies_on_gate_direction(self): qc = QuantumCircuit(2) qc.h(0) qc.ecr(0, 1) - tqc = transpile(qc, self.backend) + tqc = transpile(qc, self.backend, optimization_level=1) expected = QuantumCircuit(2) expected.u(0, 0, -math.pi, 0) expected.u(math.pi / 2, 0, 0, 1) @@ -191,7 +191,7 @@ def test_transpile_mumbai_target(self): qc.h(0) qc.cx(1, 0) qc.measure_all() - tqc = transpile(qc, backend) + tqc = transpile(qc, backend, optimization_level=1) qr = QuantumRegister(27, "q") cr = ClassicalRegister(2, "meas") expected = QuantumCircuit(qr, cr, global_phase=math.pi / 4) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index cf029d5d98c2..1dc295a02be1 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -764,7 +764,9 @@ def get_sched(qubit_idx: [int], backend): qc = circuit.QuantumCircuit(2) for idx in qubit_idx: qc.append(circuit.library.U2Gate(0, pi / 2), [idx]) - return compiler.schedule(compiler.transpile(qc, backend=backend), backend) + return compiler.schedule( + compiler.transpile(qc, backend=backend, optimization_level=1), backend + ) with pulse.build(self.backend) as schedule: with pulse.align_sequential(): @@ -784,7 +786,7 @@ def get_sched(qubit_idx: [int], backend): # prepare and schedule circuits that will be used. single_u2_qc = circuit.QuantumCircuit(2) single_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - single_u2_qc = compiler.transpile(single_u2_qc, self.backend) + single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) single_u2_sched = compiler.schedule(single_u2_qc, self.backend) # sequential context @@ -809,7 +811,7 @@ def get_sched(qubit_idx: [int], backend): triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend) + triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") # measurement diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index e1c8063a4575..9dae6c3f283a 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -1106,6 +1106,7 @@ def test_skip_target_basis_equivalences_1(self): circ, basis_gates=["id", "rz", "sx", "x", "cx"], seed_transpiler=42, + optimization_level=1, ) self.assertEqual(circ_transpiled.count_ops(), {"cx": 91, "rz": 66, "sx": 22}) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index fd6ae6a01cda..d1ea21cde544 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -2118,11 +2118,9 @@ def test_qft_plugins_qft(self, qft_plugin_name): qc.cx(1, 3) qc.append(QFTGate(3).inverse(), [0, 1, 2]) hls_config = HLSConfig(qft=[qft_plugin_name]) - basis_gates = ["cx", "u"] - qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) self.assertEqual(Operator(qc), Operator(qct)) - ops = set(qct.count_ops().keys()) - self.assertEqual(ops, {"u", "cx"}) @data("line", "full") def test_qft_line_plugin_annotated_qft(self, qft_plugin_name): @@ -2130,11 +2128,9 @@ def test_qft_line_plugin_annotated_qft(self, qft_plugin_name): qc = QuantumCircuit(4) qc.append(QFTGate(3).inverse(annotated=True).control(annotated=True), [0, 1, 2, 3]) hls_config = HLSConfig(qft=[qft_plugin_name]) - basis_gates = ["cx", "u"] - qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) self.assertEqual(Operator(qc), Operator(qct)) - ops = set(qct.count_ops().keys()) - self.assertEqual(ops, {"u", "cx"}) if __name__ == "__main__": diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 32d39304b44e..949d754573c5 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -1219,6 +1219,24 @@ def test_with_backend(self, optimization_level): pm = generate_preset_pass_manager(optimization_level, target) self.assertIsInstance(pm, PassManager) + def test_default_optimization_level(self): + """Test a pass manager is constructed with no optimization level.""" + backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP) + pm = generate_preset_pass_manager(backend=backend) + self.assertIsInstance(pm, PassManager) + + def test_default_optimization_level_backend_first_pos_arg(self): + """Test a pass manager is constructed with only a positional backend.""" + backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP) + pm = generate_preset_pass_manager(backend) + self.assertIsInstance(pm, PassManager) + + def test_default_optimization_level_target_first_pos_arg(self): + """Test a pass manager is constructed with only a positional target.""" + backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP) + pm = generate_preset_pass_manager(backend.target) + self.assertIsInstance(pm, PassManager) + @data(0, 1, 2, 3) def test_with_no_backend(self, optimization_level): """Test a passmanager is constructed with no backend and optimization level.""" diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 0a7b977162a3..4c09bf12efa8 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -195,7 +195,9 @@ def test_layout_with_classical_bits(self): rz(0) q4835[1]; """ ) - res = transpile(qc, Fake27QPulseV1(), layout_method="sabre", seed_transpiler=1234) + res = transpile( + qc, Fake27QPulseV1(), layout_method="sabre", seed_transpiler=1234, optimization_level=1 + ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( @@ -251,6 +253,7 @@ def test_layout_many_search_trials(self): layout_method="sabre", routing_method="stochastic", seed_transpiler=12345, + optimization_level=1, ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout From b362df0c4227ad0109df3e67b0ca6c1384d2bc30 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 26 Jul 2024 22:03:52 +0100 Subject: [PATCH 06/27] Allow overriding Rust toolchain in `tox` runs (#12827) It can be convenient to use different versions of the Rust toolchain for various `tox` runs. This can already be done externally to `tox` by a stateful `rustup override set ` followed by clearing the override after the tox run, but allowing the environment variable to pass through lets it be localised to just a single run. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 3ee544538a09..89dc84d1758a 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ setenv = QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE passenv = + RUSTUP_TOOLCHAIN RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL From b7d0a974b651a5a2d61932bed1119bbce1520ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:39:41 +0200 Subject: [PATCH 07/27] Primitive V1 deprecation follow-up (#12824) * Apply tweaks to deprecation messages, docstrings and release notes. * Fix missing backtick * Apply suggestions from Luciano's code review Co-authored-by: Luciano Bello * Update qiskit/primitives/statevector_sampler.py Co-authored-by: Luciano Bello * Update qiskit/primitives/backend_sampler.py Co-authored-by: Luciano Bello * Update qiskit/primitives/primitive_job.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update qiskit/primitives/base/base_estimator.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --------- Co-authored-by: Luciano Bello Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/primitives/backend_estimator.py | 39 +++++++++++-------- qiskit/primitives/backend_sampler.py | 34 +++++++++------- qiskit/primitives/base/base_estimator.py | 15 +++++-- qiskit/primitives/base/base_sampler.py | 13 +++++-- qiskit/primitives/estimator.py | 15 ++++--- qiskit/primitives/primitive_job.py | 2 +- qiskit/primitives/sampler.py | 17 +++++--- qiskit/primitives/statevector_estimator.py | 2 +- qiskit/primitives/statevector_sampler.py | 2 +- .../notes/deprecate-primitives-v1.yaml | 22 +++++++---- 10 files changed, 101 insertions(+), 60 deletions(-) diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index b217ff25665e..f6a53c02ea9e 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -9,9 +9,8 @@ # 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. -""" -Expectation value class -""" + +"""Estimator V1 implementation for an arbitrary Backend object.""" from __future__ import annotations @@ -37,7 +36,7 @@ ) from qiskit.utils.deprecation import deprecate_func -from .base import BaseEstimator, EstimatorResult +from .base import BaseEstimatorV1, EstimatorResult from .primitive_job import PrimitiveJob from .utils import _circuit_key, _observable_key, init_observable @@ -89,23 +88,29 @@ def _prepare_counts(results: list[Result]): return counts -class BackendEstimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): +class BackendEstimator(BaseEstimatorV1[PrimitiveJob[EstimatorResult]]): """Evaluates expectation value using Pauli rotation gates. The :class:`~.BackendEstimator` class is a generic implementation of the - :class:`~.BaseEstimator` interface that is used to wrap a :class:`~.BackendV2` - (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimator` API. It + :class:`~.BaseEstimatorV1` interface that is used to wrap a :class:`~.BackendV2` + (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimatorV1` API. It facilitates using backends that do not provide a native - :class:`~.BaseEstimator` implementation in places that work with - :class:`~.BaseEstimator`. However, - if you're using a provider that has a native implementation of - :class:`~.BaseEstimator`, it is a better choice to leverage that native - implementation as it will likely include additional optimizations and be - a more efficient implementation. The generic nature of this class - precludes doing any provider- or backend-specific optimizations. + :class:`~.BaseEstimatorV1` implementation in places that work with + :class:`~.BaseEstimatorV1`. + However, if you're using a provider that has a native implementation of + :class:`~.BaseEstimatorV1` or :class:`~.BaseEstimatorV2`, it is a better + choice to leverage that native implementation as it will likely include + additional optimizations and be a more efficient implementation. + The generic nature of this class precludes doing any provider- or + backend-specific optimizations. """ - @deprecate_func(since="1.2", additional_msg="Use BackendEstimatorV2 instead.") + @deprecate_func( + since="1.2", + additional_msg="All implementations of the `BaseEstimatorV1` interface " + "have been deprecated in favor of their V2 counterparts. " + "The V2 alternative for the `BackendEstimator` class is `BackendEstimatorV2`.", + ) def __init__( self, backend: BackendV1 | BackendV2, @@ -114,10 +119,10 @@ def __init__( bound_pass_manager: PassManager | None = None, skip_transpilation: bool = False, ): - """Initialize a new BackendEstimator instance + """Initialize a new BackendEstimator (V1) instance Args: - backend: Required: the backend to run the primitive on + backend: (required) the backend to run the primitive on options: Default options. abelian_grouping: Whether the observable should be grouped into commuting diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 98592e079cb7..213b9701a556 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Sampler implementation for an arbitrary Backend object.""" +"""Sampler V1 implementation for an arbitrary Backend object.""" from __future__ import annotations @@ -26,28 +26,34 @@ from qiskit.utils.deprecation import deprecate_func from .backend_estimator import _prepare_counts, _run_circuits -from .base import BaseSampler, SamplerResult +from .base import BaseSamplerV1, SamplerResult from .primitive_job import PrimitiveJob from .utils import _circuit_key -class BackendSampler(BaseSampler[PrimitiveJob[SamplerResult]]): - """A :class:`~.BaseSampler` implementation that provides an interface for - leveraging the sampler interface from any backend. +class BackendSampler(BaseSamplerV1[PrimitiveJob[SamplerResult]]): + """A :class:`~.BaseSamplerV1` implementation that provides a wrapper for + leveraging the Sampler V1 interface from any backend. This class provides a sampler interface from any backend and doesn't do any measurement mitigation, it just computes the probability distribution from the counts. It facilitates using backends that do not provide a - native :class:`~.BaseSampler` implementation in places that work with - :class:`~.BaseSampler`. + native :class:`~.BaseSamplerV1` implementation in places that work with + :class:`~.BaseSamplerV1`. However, if you're using a provider that has a native implementation of - :class:`~.BaseSampler`, it is a better choice to leverage that native - implementation as it will likely include additional optimizations and be - a more efficient implementation. The generic nature of this class - precludes doing any provider- or backend-specific optimizations. + :class:`~.BaseSamplerV1` or :class:`~.BaseESamplerV2`, it is a better + choice to leverage that native implementation as it will likely include + additional optimizations and be a more efficient implementation. + The generic nature of this class precludes doing any provider- or + backend-specific optimizations. """ - @deprecate_func(since="1.2", additional_msg="Use BackendSamplerV2 instead.") + @deprecate_func( + since="1.2", + additional_msg="All implementations of the `BaseSamplerV1` interface " + "have been deprecated in favor of their V2 counterparts. " + "The V2 alternative for the `BackendSampler` class is `BackendSamplerV2`.", + ) def __init__( self, backend: BackendV1 | BackendV2, @@ -55,10 +61,10 @@ def __init__( bound_pass_manager: PassManager | None = None, skip_transpilation: bool = False, ): - """Initialize a new BackendSampler + """Initialize a new BackendSampler (V1) instance Args: - backend: Required: the backend to run the sampler primitive on + backend: (required) the backend to run the sampler primitive on options: Default options. bound_pass_manager: An optional pass manager to run after parameter binding. diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 33ec40300b2a..d3d364f9951a 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Base Estimator Classes""" +"""Base Estimator V1 and V2 classes""" from __future__ import annotations @@ -110,7 +110,7 @@ def __init__( options: dict | None = None, ): """ - Creating an instance of an Estimator, or using one in a ``with`` context opens a session that + Creating an instance of an Estimator V1, or using one in a ``with`` context opens a session that holds resources until the instance is ``close()`` ed or the context is exited. Args: @@ -189,12 +189,19 @@ def _run( class BaseEstimator(BaseEstimatorV1[T]): - """DEPRECATED. Type alias of Estimator V1 base class. + """DEPRECATED. Type alias for Estimator V1 base class. See :class:`.BaseEstimatorV1` for details. """ - @deprecate_func(since="1.2", additional_msg="Use BaseEstimatorV2 instead.") + @deprecate_func( + since="1.2", + additional_msg="The `BaseEstimator` class is a type alias for the `BaseEstimatorV1` " + "interface that has been deprecated in favor of explicitly versioned interface classes. " + "It is recommended to migrate all implementations to use `BaseEstimatorV2`. " + "However, for implementations incompatible with `BaseEstimatorV2`, `BaseEstimator` can " + "be replaced with the explicitly versioned `BaseEstimatorV1` class.", + ) def __init__( self, *, diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 81a1754ae35b..65d87d86b077 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Base Sampler Classes""" +"""Base Sampler V1 and V2 classes""" from __future__ import annotations @@ -152,12 +152,19 @@ def _run( class BaseSampler(BaseSamplerV1[T]): - """DEPRECATED. Type alias of Sampler V1 base class + """DEPRECATED. Type alias for Sampler V1 base class See :class:`.BaseSamplerV1` for details. """ - @deprecate_func(since="1.2", additional_msg="Use BaseSamplerV2 instead.") + @deprecate_func( + since="1.2", + additional_msg="The `BaseSampler` class is a type alias for the `BaseSamplerV1` " + "interface that has been deprecated in favor of explicitly versioned interface classes. " + "It is recommended to migrate all implementations to use `BaseSamplerV2`. " + "However, for implementations incompatible with `BaseSamplerV2`, `BaseSampler` can " + "be replaced with the explicitly versioned `BaseSamplerV1` class.", + ) def __init__( self, *, diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 874b631379b2..1ca1529852c3 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Estimator class +Estimator V1 reference implementation """ from __future__ import annotations @@ -26,7 +26,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func -from .base import BaseEstimator, EstimatorResult +from .base import BaseEstimatorV1, EstimatorResult from .primitive_job import PrimitiveJob from .utils import ( _circuit_key, @@ -36,9 +36,9 @@ ) -class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): +class Estimator(BaseEstimatorV1[PrimitiveJob[EstimatorResult]]): """ - Reference implementation of :class:`BaseEstimator`. + Reference implementation of :class:`BaseEstimatorV1`. :Run Options: @@ -52,7 +52,12 @@ class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): this option is ignored. """ - @deprecate_func(since="1.2", additional_msg="Use StatevectorEstimator instead.") + @deprecate_func( + since="1.2", + additional_msg="All implementations of the `BaseEstimatorV1` interface " + "have been deprecated in favor of their V2 counterparts. " + "The V2 alternative for the `Estimator` class is `StatevectorEstimator`.", + ) def __init__(self, *, options: dict | None = None): """ Args: diff --git a/qiskit/primitives/primitive_job.py b/qiskit/primitives/primitive_job.py index 5dddda27d98b..64ab8d016095 100644 --- a/qiskit/primitives/primitive_job.py +++ b/qiskit/primitives/primitive_job.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Job implementation for the reference implementations of Primitives. +Job for the reference implementations of Primitives V1 and V2. """ import uuid diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index da0b4ed4d003..d93db4f04116 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Sampler class +Sampler V1 reference implementation """ from __future__ import annotations @@ -26,7 +26,7 @@ from qiskit.result import QuasiDistribution from qiskit.utils.deprecation import deprecate_func -from .base import BaseSampler, SamplerResult +from .base import BaseSamplerV1, SamplerResult from .primitive_job import PrimitiveJob from .utils import ( _circuit_key, @@ -36,11 +36,11 @@ ) -class Sampler(BaseSampler[PrimitiveJob[SamplerResult]]): +class Sampler(BaseSamplerV1[PrimitiveJob[SamplerResult]]): """ - Sampler class. + Sampler V1 class. - :class:`~Sampler` is a reference implementation of :class:`~BaseSampler`. + :class:`~Sampler` is a reference implementation of :class:`~BaseSamplerV1`. :Run Options: @@ -53,7 +53,12 @@ class Sampler(BaseSampler[PrimitiveJob[SamplerResult]]): option is ignored. """ - @deprecate_func(since="1.2", additional_msg="Use StatevectorSampler instead.") + @deprecate_func( + since="1.2", + additional_msg="All implementations of the `BaseSamplerV1` interface " + "have been deprecated in favor of their V2 counterparts. " + "The V2 alternative for the `Sampler` class is `StatevectorSampler`.", + ) def __init__(self, *, options: dict | None = None): """ Args: diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index a5dc029edf73..722291bcf42f 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Estimator class +Statevector Estimator V2 class """ from __future__ import annotations diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index 90fe452ad124..7488faa284de 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Statevector Sampler class +Statevector Sampler V2 class """ from __future__ import annotations diff --git a/releasenotes/notes/deprecate-primitives-v1.yaml b/releasenotes/notes/deprecate-primitives-v1.yaml index 9526c12d6eca..3d0335d358cb 100644 --- a/releasenotes/notes/deprecate-primitives-v1.yaml +++ b/releasenotes/notes/deprecate-primitives-v1.yaml @@ -1,17 +1,23 @@ --- deprecations_primitives: - | - Primitives V1 is now deprecated and will be removed in no less than 3 months from the release date. + Primitive V1 implementations and V1-exclusive non-versioned type aliases are now + deprecated in favor of their V2 counterparts. The deprecation is extended to the + following classes implementing V1 interfaces: - The following Primitives V1 classes are deprecated: + * :class:`.Estimator`, in favor of the V2 equivalent, :class:`.StatevectorEstimator` + * :class:`.Sampler`, in favor of the V2 equivalent, :class:`.StatevectorSampler` + * :class:`.BackendEstimator`, in favor of the V2 equivalent, :class:`.BackendEstimatorV2` + * :class:`.BackendSampler`, in favor of the V2 equivalent, :class:`.BackendSamplerV2` - * :class:`.BaseEstimator`, use :class:`.BaseEstimatorV2` instead, - * :class:`.BaseSampler`, use :class:`.BaseSamplerV2` instead, - * :class:`.Estimator`, use :class:`.StatevectorEstimator` instead, - * :class:`.Sampler`, use :class:`.StatevectorSampler` instead, - * :class:`.BackendEstimator`, use :class:`.BackendEstimatorV2` instead, - * :class:`.BackendSampler`, use :class:`.BackendSamplerV2` instead, + As well as the following non-versioned type aliases: + + * :class:`.BaseEstimator`, alias for :class:`.BaseEstimatorV1` + * :class:`.BaseSampler`, alias for :class:`.BaseSamplerV1` + This deprecation does NOT affect the explicitly-versioned :class:`BaseEstimatorV1` + and :class:`BaseSamplerV1` abstract + interface definitions or related result and job classes. In addition, the following utility functions are deprecated: From 0c03808cd177c857a3458df7e58ca9e800185577 Mon Sep 17 00:00:00 2001 From: aeddins-ibm <60495383+aeddins-ibm@users.noreply.github.com> Date: Mon, 29 Jul 2024 05:53:43 -0400 Subject: [PATCH 08/27] Bitarray postselect (#12693) * define BitArray.postselect() * add test for BitArray.postselect() * lint * remove redundant docstring text * Update qiskit/primitives/containers/bit_array.py Co-authored-by: Ian Hincks * docstring ticks (BitArray.postselect()) Co-authored-by: Ian Hincks * Simpler tests for BitArray.postselect * lint * add release note * check postselect() arg lengths match * fix postselect tests - fix bugs with checking that ValueError is raised. - addtionally run all tests on a "flat" data input * lint * Fix type-hint We immediately check the lengths of these args, so they should be Sequences, not Iterables. * remove spurious print() * lint * lint * use bitwise operations for faster postselect - Also added support for negative indices - Also updated tests * remove spurious print() * end final line of release note * try to fix docstring formatting * fix bitarray test assertion Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * disallow postselect positional kwarg Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * fix numpy dtype args * Simpler kwarg: "assume_unique" * lint (line too long) * simplification: remove assume_unique kwarg * improve misleading comment * raise IndexError if indices out of range - Change ValueError to IndexError. - Add check for out-of-range negative indices. - Simplify use of mod - Update test conditions (include checks for off-by-one errors) * lint * add negative-contradiction test * Update docstring with IndexErrors * lint * change slice_bits error from ValueError to IndexError * update slice_bits test to use IndexError * change ValueError to IndexError in slice_shots also update tests for this error * update error type in slice_shots docstring * Revert ValueError to IndexError changes Reverting these changes as they will instead be made in a separate PR. This reverts commit 8f3217838c6632d30ef300445fcca1590454b536. Revert "update error type in slice_shots docstring" This reverts commit 50545efbf26f6fac72c7c00919ae0995b8464ba6. Revert "change ValueError to IndexError in slice_shots" This reverts commit c4becd9b0e4363797331157b176c1603dabc40c9. Revert "update slice_bits test to use IndexError" This reverts commit c2b00390da40b12d5821476de850803da2b72a69. * fix docstring formatting Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * allow selection to be int instead of bool * In tests, give selection as type int * lint * add example to release note * fix typo in test case * add check of test Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * lint --------- Co-authored-by: Ian Hincks Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/primitives/containers/bit_array.py | 91 +++++++++++++++++++ .../bitarray-postselect-659b8f7801ccaa60.yaml | 11 +++ .../primitives/containers/test_bit_array.py | 79 ++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index 11cd91a96521..29ff3240f3bf 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -470,6 +470,97 @@ def slice_shots(self, indices: int | Sequence[int]) -> "BitArray": arr = arr[..., indices, :] return BitArray(arr, self.num_bits) + def postselect( + self, + indices: Sequence[int] | int, + selection: Sequence[bool | int] | bool | int, + ) -> BitArray: + """Post-select this bit array based on sliced equality with a given bitstring. + + .. note:: + If this bit array contains any shape axes, it is first flattened into a long list of shots + before applying post-selection. This is done because :class:`~BitArray` cannot handle + ragged numbers of shots across axes. + + Args: + indices: A list of the indices of the cbits on which to postselect. + If this bit array was produced by a sampler, then an index ``i`` corresponds to the + :class:`~.ClassicalRegister` location ``creg[i]`` (as in :meth:`~slice_bits`). + Negative indices are allowed. + + selection: A list of binary values (will be cast to ``bool``) of length matching + ``indices``, with ``indices[i]`` corresponding to ``selection[i]``. Shots will be + discarded unless all cbits specified by ``indices`` have the values given by + ``selection``. + + Returns: + A new bit array with ``shape=(), num_bits=data.num_bits, num_shots<=data.num_shots``. + + Raises: + IndexError: If ``max(indices)`` is greater than or equal to :attr:`num_bits`. + IndexError: If ``min(indices)`` is less than negative :attr:`num_bits`. + ValueError: If the lengths of ``selection`` and ``indices`` do not match. + """ + if isinstance(indices, int): + indices = (indices,) + if isinstance(selection, (bool, int)): + selection = (selection,) + selection = np.asarray(selection, dtype=bool) + + num_indices = len(indices) + + if len(selection) != num_indices: + raise ValueError("Lengths of indices and selection do not match.") + + num_bytes = self._array.shape[-1] + indices = np.asarray(indices) + + if num_indices > 0: + if indices.max() >= self.num_bits: + raise IndexError( + f"index {int(indices.max())} out of bounds for the number of bits {self.num_bits}." + ) + if indices.min() < -self.num_bits: + raise IndexError( + f"index {int(indices.min())} out of bounds for the number of bits {self.num_bits}." + ) + + flattened = self.reshape((), self.size * self.num_shots) + + # If no conditions, keep all data, but flatten as promised: + if num_indices == 0: + return flattened + + # Make negative bit indices positive: + indices %= self.num_bits + + # Handle special-case of contradictory conditions: + if np.intersect1d(indices[selection], indices[np.logical_not(selection)]).size > 0: + return BitArray(np.empty((0, num_bytes), dtype=np.uint8), num_bits=self.num_bits) + + # Recall that creg[0] is the LSb: + byte_significance, bit_significance = np.divmod(indices, 8) + # least-significant byte is at last position: + byte_idx = (num_bytes - 1) - byte_significance + # least-significant bit is at position 0: + bit_offset = bit_significance.astype(np.uint8) + + # Get bitpacked representation of `indices` (bitmask): + bitmask = np.zeros(num_bytes, dtype=np.uint8) + np.bitwise_or.at(bitmask, byte_idx, np.uint8(1) << bit_offset) + + # Get bitpacked representation of `selection` (desired bitstring): + selection_bytes = np.zeros(num_bytes, dtype=np.uint8) + ## This assumes no contradictions present, since those were already checked for: + np.bitwise_or.at( + selection_bytes, byte_idx, np.asarray(selection, dtype=np.uint8) << bit_offset + ) + + return BitArray( + flattened._array[((flattened._array & bitmask) == selection_bytes).all(axis=-1)], + num_bits=self.num_bits, + ) + def expectation_values(self, observables: ObservablesArrayLike) -> NDArray[np.float64]: """Compute the expectation values of the provided observables, broadcasted against this bit array. diff --git a/releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml b/releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml new file mode 100644 index 000000000000..33ce17bafa8d --- /dev/null +++ b/releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml @@ -0,0 +1,11 @@ +--- +features_primitives: + - | + Added a new method :meth:`.BitArray.postselect` that returns all shots containing specified bit values. + Example usage:: + + from qiskit.primitives.containers import BitArray + + ba = BitArray.from_counts({'110': 2, '100': 4, '000': 3}) + print(ba.postselect([0,2], [0,1]).get_counts()) + # {'110': 2, '100': 4} diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index 4aeeba854b33..bd41d127689d 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -719,3 +719,82 @@ def test_expectation_values(self): _ = ba.expectation_values("Z") with self.assertRaisesRegex(ValueError, "is not diagonal"): _ = ba.expectation_values("X" * ba.num_bits) + + def test_postselection(self): + """Test the postselection method.""" + + flat_data = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + ], + dtype=bool, + ) + + shaped_data = np.array( + [ + [ + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + ], + [ + [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + ], + ] + ], + dtype=bool, + ) + + for dataname, bool_array in zip(["flat", "shaped"], [flat_data, shaped_data]): + + bit_array = BitArray.from_bool_array(bool_array, order="little") + # indices value of i <-> creg[i] <-> bool_array[..., i] + + num_bits = bool_array.shape[-1] + bool_array = bool_array.reshape(-1, num_bits) + + test_cases = [ + ("basic", [0, 1], [0, 0]), + ("multibyte", [0, 9], [0, 1]), + ("repeated", [5, 5, 5], [0, 0, 0]), + ("contradict", [5, 5, 5], [1, 0, 0]), + ("unsorted", [5, 0, 9, 3], [1, 0, 1, 0]), + ("negative", [-5, 1, -2, -10], [1, 0, 1, 0]), + ("negcontradict", [4, -6], [1, 0]), + ("trivial", [], []), + ("bareindex", 6, 0), + ] + + for name, indices, selection in test_cases: + with self.subTest("_".join([dataname, name])): + postselected_bools = np.unpackbits( + bit_array.postselect(indices, selection).array[:, ::-1], + count=num_bits, + axis=-1, + bitorder="little", + ).astype(bool) + if isinstance(indices, int): + indices = (indices,) + if isinstance(selection, bool): + selection = (selection,) + answer = bool_array[np.all(bool_array[:, indices] == selection, axis=-1)] + if name in ["contradict", "negcontradict"]: + self.assertEqual(len(answer), 0) + else: + self.assertGreater(len(answer), 0) + np.testing.assert_equal(postselected_bools, answer) + + error_cases = [ + ("aboverange", [0, 6, 10], [True, True, False], IndexError), + ("belowrange", [0, 6, -11], [True, True, False], IndexError), + ("mismatch", [0, 1, 2], [False, False], ValueError), + ] + for name, indices, selection, error in error_cases: + with self.subTest(dataname + "_" + name): + with self.assertRaises(error): + bit_array.postselect(indices, selection) From 7cd2c414b0663765fe447f5fb91300ca6865f333 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 29 Jul 2024 12:09:40 +0200 Subject: [PATCH 09/27] Deprecate fake backends based on BackendV1 (#12660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deprecate qobj and assemble * reno * first attempt on Fake1QV2 * deprecate Fake1Q * Fake1QV2 should not have two-qubit gates Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * test.python.circuit.test_parameters.TestParameters * test.python.providers.fake_provider.test_fake_backends.FakeBackendsTest * test.python.providers.test_fake_backends.TestFakeBackends * test.python.result.test_mitigators * test.python.transpiler.test_preset_passmanagers * test.python.transpiler.test_target * test.python.transpiler.test_unitary_synthesis * test.python.transpiler.test_vf2_layout * test.python.transpiler.test_vf2_post_layout * test/python/visualization/test_circuit_latex * test.python.transpiler.test_sabre_layout * test.python.transpiler.test_sabre_layout * test.python.transpiler.test_pulse_gate_pass * test.python.scheduler.test_basic_scheduler.TestBasicSchedule * test.python.pulse.test_transforms * test.python.pulse.test_schedule * test.python.pulse.test_macros * test.python.pulse.test_instruction_schedule_map * test.python.pulse.test_block * test.python.circuit.test_scheduled_circuit * test.python.transpiler.test_calibrationbuilder * test.python.providers.test_backendconfiguration * test.python.compiler.test_transpiler * test.python.transpiler.test_passmanager_run * test.python.transpiler.test_passmanager_config.TestPassManagerConfig.test_from_backend_and_user * test.python.transpiler.test_passmanager_config * test.python.primitives.test_backend_estimator.TestBackendEstimator * test.python.circuit.test_scheduled_circuit.TestScheduledCircuit.test_schedule_circuit_when_backend_tells_dt * test.python.circuit.test_scheduled_circuit * test.python.transpiler.test_vf2_layout * shallow deprecation of assemble * test.python.compiler.test_disassembler * fakebackend pulse * test.python.circuit.test_parameters * PulseQobjInstruction is used by GenericBackendV2 * test.python.scheduler.test_basic_scheduler * test.python.result.test_result * test.python.pulse.test_calibration_entries * test.python.compiler.test_assembler * test.python.transpiler.test_star_prerouting * test.python.pulse.test_instruction_schedule_map * test.python.providers.basic_provider.test_basic_simulator * test.python.primitives.test_backend_sampler_v2 * test.python.compiler.test_disassembler * test.python.compiler.test_compiler * test.python.circuit.test_scheduled_circuit * test.python.providers.test_fake_backends * test.python.circuit.test_unitary * test.python.transpiler.test_sabre_swap * test.python.providers.fake_provider.test_fake_backends * Aer using Provider ABC * aer warnings * reno * another pass on reno * test.python.pulse * test.python.compiler.test_compiler * add module to fiterwarning * test.python.compiler.test_transpiler * fixing obscure expcetion handleing for comparison * test.python.transpiler.test_stochastic_swap test.python.transpiler.test_sabre_swap * test.python.transpiler.test_echo_rzx_weyl_decomposition * test.python.transpiler.test_instruction_durations * test.python.providers.test_backendproperties * test.python.qpy.test_circuit_load_from_qpy * test.python.providers.test_pulse_defaults * test.python.primitives.test_backend_sampler_v2 * test.python.primitives.test_backend_sampler * test.python.compiler.test_scheduler * test/python/compiler/test_scheduler.py * test.python.compiler.test_disassembler * test.python.compiler.test_assembler * test.python.compiler.test_sequencer * test.python.compiler.test_transpiler * test.python.primitives.test_primitive * better depreaction handleling from Aer * test.python.primitives.test_backend_estimator_v2 * test.python.compiler.test_compiler * ignore warnings, because the output is otherwise very verbose * ignore to avoid fludding the CI log * seeding all GenericBackendV2 * test.python.visualization.test_gate_map * deprecation warnings: once * default * default * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * remove catch * new deprecate warning message * lint qiskit/assembler/assemble_circuits.py * concurrency warnings * ignore aer warnings * Update test/python/providers/fake_provider/test_fake_backends.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/circuit/test_parameters.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/providers/models/pulsedefaults.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/providers/fake_provider/test_fake_backends.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/providers/fake_provider/test_generic_backend_v2.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * lint * https://github.com/Qiskit/qiskit/pull/12649#discussion_r1686717954 * https://github.com/Qiskit/qiskit/pull/12649#discussion_r1686717732 * Update test/python/transpiler/test_sabre_swap.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/providers/models/pulsedefaults.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * ignore Treating CircuitInstruction... * another unnecessary catch from aer * another unnecessary catch from aer, again * removing more unnecesary catches * less lines * seeding * remove those comments * test.python.compiler.test_transpiler.TestTranspile.test_scheduling_timing_constraints * tokyo cmap * test.python.circuit.test_scheduled_circuit.TestScheduledCircuit.test_schedule_circuit_when_backend_tells_dt * comment in test_sequencer.py * test.python.compiler.test_transpiler.TestPostTranspileIntegration.test_qasm3_output * Update test/python/primitives/test_primitive.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * test/python/providers/test_backendconfiguration.py:30 * test.python.primitives.test_backend_sampler * restore the assertWarns, waiting for #12818 * use legacy_cmaps instead of explict coupling maps * use more legacy_map * KYOTO_CMAP * more legacy_cmap * https://github.com/Qiskit/qiskit/issues/12832 --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../providers/fake_provider/fake_backend.py | 9 +- .../fake_provider/generic_backend_v2.py | 16 +- qiskit/visualization/circuit/_utils.py | 2 +- test/python/circuit/test_parameters.py | 33 +- test/python/circuit/test_scheduled_circuit.py | 24 +- test/python/compiler/test_assembler.py | 33 +- test/python/compiler/test_compiler.py | 5 +- test/python/compiler/test_disassembler.py | 3 +- test/python/compiler/test_scheduler.py | 10 +- test/python/compiler/test_sequencer.py | 5 +- test/python/compiler/test_transpiler.py | 95 ++++-- test/python/legacy_cmaps.py | 148 +++++++++ .../primitives/test_backend_estimator.py | 21 +- .../primitives/test_backend_estimator_v2.py | 5 +- .../python/primitives/test_backend_sampler.py | 34 +- .../primitives/test_backend_sampler_v2.py | 7 +- test/python/primitives/test_primitive.py | 8 +- .../fake_provider/test_fake_backends.py | 7 +- .../fake_provider/test_generic_backend_v2.py | 27 +- test/python/providers/test_backend_v2.py | 14 +- .../providers/test_backendconfiguration.py | 18 +- .../providers/test_backendproperties.py | 7 +- test/python/providers/test_backendstatus.py | 3 +- test/python/providers/test_fake_backends.py | 58 ++-- test/python/providers/test_pulse_defaults.py | 11 +- test/python/pulse/test_block.py | 3 +- test/python/pulse/test_builder.py | 6 +- test/python/pulse/test_builder_v2.py | 4 +- .../pulse/test_instruction_schedule_map.py | 35 +- test/python/pulse/test_macros.py | 15 +- test/python/pulse/test_schedule.py | 3 +- test/python/pulse/test_transforms.py | 6 +- test/python/qpy/test_circuit_load_from_qpy.py | 16 +- test/python/result/test_mitigators.py | 33 +- test/python/scheduler/test_basic_scheduler.py | 19 +- test/python/transpiler/test_1q.py | 65 +++- .../transpiler/test_calibrationbuilder.py | 19 +- .../test_echo_rzx_weyl_decomposition.py | 7 +- .../transpiler/test_instruction_durations.py | 8 +- .../transpiler/test_passmanager_config.py | 44 ++- .../python/transpiler/test_passmanager_run.py | 66 +++- .../transpiler/test_preset_passmanagers.py | 31 +- .../python/transpiler/test_pulse_gate_pass.py | 61 +++- test/python/transpiler/test_sabre_layout.py | 8 +- test/python/transpiler/test_sabre_swap.py | 16 +- .../python/transpiler/test_star_prerouting.py | 5 +- .../python/transpiler/test_stochastic_swap.py | 11 +- test/python/transpiler/test_target.py | 12 +- .../transpiler/test_unitary_synthesis.py | 39 ++- test/python/transpiler/test_vf2_layout.py | 38 ++- .../python/transpiler/test_vf2_post_layout.py | 35 +- .../visualization/test_circuit_drawer.py | 2 +- .../visualization/test_circuit_latex.py | 6 +- test/python/visualization/test_gate_map.py | 307 +++++++++++++++++- 54 files changed, 1210 insertions(+), 313 deletions(-) diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index 4a638f315574..21d221b68c04 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -23,7 +23,7 @@ from qiskit.providers import BackendV1 from qiskit import pulse from qiskit.exceptions import QiskitError -from qiskit.utils import optionals as _optionals +from qiskit.utils import optionals as _optionals, deprecate_func from qiskit.providers import basic_provider @@ -39,6 +39,13 @@ def __init__(self, token="123456", url="https://"): class FakeBackend(BackendV1): """This is a dummy backend just for testing purposes.""" + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="Fake backends using BackendV1 are deprecated in favor of " + ":class:`.GenericBackendV2`. You can convert BackendV1 to " + ":class:`.BackendV2` with :class:`.BackendV2Converter`.", + ) def __init__(self, configuration, time_alive=10): """FakeBackend initializer. diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index c3de76cbd0e6..6374a0b0b60a 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -567,18 +567,12 @@ def _setup_sim(self) -> None: @classmethod def _default_options(cls) -> Options: - with warnings.catch_warnings(): # TODO remove catch once aer release without Provider ABC - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".+abstract Provider and ProviderV1.+", - ) - if _optionals.HAS_AER: - from qiskit_aer import AerSimulator + if _optionals.HAS_AER: + from qiskit_aer import AerSimulator - return AerSimulator._default_options() - else: - return BasicSimulator._default_options() + return AerSimulator._default_options() + else: + return BasicSimulator._default_options() def drive_channel(self, qubit: int): drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index 2077a3891542..e6ee03905d27 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -387,7 +387,7 @@ def _get_valid_justify_arg(justify): warn( f"Setting QuantumCircuit.draw()’s or circuit_drawer()'s justify argument: {justify}, to a " "value other than 'left', 'right', 'none' or None (='left'). Default 'left' will be used. " - "Support for invalid justify arguments is deprecated as of qiskit 1.2.0. Starting no " + "Support for invalid justify arguments is deprecated as of Qiskit 1.2.0. Starting no " "earlier than 3 months after the release date, invalid arguments will error.", DeprecationWarning, 2, diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 4f989767164f..c2df1a423ec4 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -31,10 +31,11 @@ from qiskit.compiler import assemble, transpile from qiskit import pulse from qiskit.quantum_info import Operator -from qiskit.providers.fake_provider import Fake5QV1 +from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 from qiskit.providers.basic_provider import BasicSimulator from qiskit.utils import parallel_map from test import QiskitTestCase, combine # pylint: disable=wrong-import-order +from ..legacy_cmaps import BOGOTA_CMAP def raise_if_parameter_table_invalid(circuit): @@ -1074,6 +1075,26 @@ def test_transpiling_multiple_parameterized_circuits(self): self.assertTrue(len(job.result().results), 2) + @data(0, 1, 2, 3) + def test_transpile_across_optimization_levelsV1(self, opt_level): + """Verify parameterized circuits can be transpiled with all default pass managers. + To remove once Fake5QV1 gets removed""" + + qc = QuantumCircuit(5, 5) + + theta = Parameter("theta") + phi = Parameter("phi") + + qc.rx(theta, 0) + qc.x(0) + for i in range(5 - 1): + qc.rxx(phi, i, i + 1) + + qc.measure(range(5 - 1), range(5 - 1)) + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + transpile(qc, backend, optimization_level=opt_level) + @data(0, 1, 2, 3) def test_transpile_across_optimization_levels(self, opt_level): """Verify parameterized circuits can be transpiled with all default pass managers.""" @@ -1090,7 +1111,15 @@ def test_transpile_across_optimization_levels(self, opt_level): qc.measure(range(5 - 1), range(5 - 1)) - transpile(qc, Fake5QV1(), optimization_level=opt_level) + transpile( + qc, + GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + seed=42, + ), + optimization_level=opt_level, + ) def test_repeated_gates_to_dag_and_back(self): """Verify circuits with repeated parameterized gates can be converted diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index c3b3158d7b85..c934b5b5e04c 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -33,8 +33,9 @@ class TestScheduledCircuit(QiskitTestCase): def setUp(self): super().setUp() - self.backend_with_dt = Fake27QPulseV1() - self.backend_without_dt = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + self.backend_with_dt = Fake27QPulseV1() + self.backend_without_dt = Fake27QPulseV1() delattr(self.backend_without_dt.configuration(), "dt") # Remove timing constraints from the backends (alignment values, # granularity and min_length), so that these values will default @@ -50,21 +51,24 @@ def setUp(self): def test_schedule_circuit_when_backend_tells_dt(self): """dt is known to transpiler by backend""" qc = QuantumCircuit(2) - qc.delay(0.1, 0, unit="ms") # 450000[dt] + qc.delay(0.1, 0, unit="ms") # 450450[dt] qc.delay(100, 0, unit="ns") # 450[dt] - qc.h(0) # 160[dt] - qc.h(1) # 160[dt] - sc = transpile(qc, self.backend_with_dt, scheduling_method="alap", layout_method="trivial") - self.assertEqual(sc.duration, 450546) + qc.h(0) # 195[dt] + qc.h(1) # 210[dt] + + backend = GenericBackendV2(2, calibrate_instructions=True, seed=42) + + sc = transpile(qc, backend, scheduling_method="alap", layout_method="trivial") + self.assertEqual(sc.duration, 451095) self.assertEqual(sc.unit, "dt") self.assertEqual(sc.data[0].operation.name, "delay") - self.assertEqual(sc.data[0].operation.duration, 450450) + self.assertEqual(sc.data[0].operation.duration, 450900) self.assertEqual(sc.data[0].operation.unit, "dt") self.assertEqual(sc.data[1].operation.name, "rz") self.assertEqual(sc.data[1].operation.duration, 0) self.assertEqual(sc.data[1].operation.unit, "dt") self.assertEqual(sc.data[4].operation.name, "delay") - self.assertEqual(sc.data[4].operation.duration, 450450) + self.assertEqual(sc.data[4].operation.duration, 450885) self.assertEqual(sc.data[4].operation.unit, "dt") def test_schedule_circuit_when_transpile_option_tells_dt(self): @@ -301,7 +305,7 @@ def test_convert_duration_to_dt(self): """Test that circuit duration unit conversion is applied only when necessary. Tests fix for bug reported in PR #11782.""" - backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=10) + backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) schedule_config = ScheduleConfig( inst_map=backend.target.instruction_schedule_map(), meas_map=backend.meas_map, diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index ff1f86f228db..c333ce9ace22 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -63,7 +63,8 @@ def setUp(self): self.circ.cx(qr[0], qr[1]) self.circ.measure(qr, cr) - self.backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + self.backend = Fake5QV1() self.backend_config = self.backend.configuration() self.num_qubits = self.backend_config.n_qubits @@ -591,7 +592,8 @@ def test_pulse_gates_with_parameteric_pulses(self): circ.h(0) circ.add_calibration("h", [0], custom_h_schedule) - backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() backend.configuration().parametric_pulses = ["drag"] with self.assertWarns(DeprecationWarning): qobj = assemble(circ, backend) @@ -974,7 +976,8 @@ class TestPulseAssembler(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.backend_config = self.backend.configuration() test_pulse = pulse.Waveform( @@ -1282,7 +1285,8 @@ def test_pulse_name_conflicts(self): def test_pulse_name_conflicts_in_other_schedule(self): """Test two pulses with the same name in different schedule can be resolved.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() defaults = backend.defaults() schedules = [] @@ -1390,7 +1394,8 @@ def test_assemble_parametric(self): ) << sched.duration ) - backend = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse3Q() backend.configuration().parametric_pulses = [ "gaussian", "drag", @@ -1437,7 +1442,8 @@ def test_assemble_parametric_unsupported(self): ) sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) - backend = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse3Q() backend.configuration().parametric_pulses = ["something_extra"] with self.assertWarns(DeprecationWarning): @@ -1449,7 +1455,8 @@ def test_assemble_parametric_unsupported(self): def test_assemble_parametric_pulse_kwarg_with_backend_setting(self): """Test that parametric pulses respect the kwarg over backend""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() qc = QuantumCircuit(1, 1) qc.x(0) @@ -1465,7 +1472,8 @@ def test_assemble_parametric_pulse_kwarg_with_backend_setting(self): def test_assemble_parametric_pulse_kwarg_empty_list_with_backend_setting(self): """Test that parametric pulses respect the kwarg as empty list over backend""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() qc = QuantumCircuit(1, 1) qc.x(0) @@ -1822,7 +1830,8 @@ def setUp(self): super().setUp() self.schedule = pulse.Schedule(name="fake_experiment") - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.config = self.backend.configuration() self.defaults = self.backend.defaults() self.qubit_lo_freq = list(self.defaults.qubit_freq_est) @@ -1963,7 +1972,8 @@ def test_missing_lo_ranges(self): def test_unsupported_meas_level(self): """Test that assembly raises an error if meas_level is not supported""" - backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() backend.configuration().meas_levels = [1, 2] with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): assemble( @@ -1983,7 +1993,8 @@ def test_unsupported_meas_level(self): def test_single_and_deprecated_acquire_styles(self): """Test that acquires are identically combined with Acquires that take a single channel.""" - backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() new_style_schedule = Schedule() acq_dur = 1200 for i in range(2): diff --git a/test/python/compiler/test_compiler.py b/test/python/compiler/test_compiler.py index 1479fde88cec..32d33ba3e56c 100644 --- a/test/python/compiler/test_compiler.py +++ b/test/python/compiler/test_compiler.py @@ -188,7 +188,6 @@ def test_example_swap_bits(self): def test_parallel_compile(self): """Trigger parallel routines in compile.""" - backend = Fake20QV1() qr = QuantumRegister(16) cr = ClassicalRegister(2) qc = QuantumCircuit(qr, cr) @@ -198,6 +197,7 @@ def test_parallel_compile(self): qc.measure(qr[5], cr[0]) qlist = [qc for k in range(10)] with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() qobj = assemble(transpile(qlist, backend=backend)) self.assertEqual(len(qobj.experiments), 10) @@ -500,7 +500,8 @@ def test_yzy_zyz_cases(self): See: https://github.com/Qiskit/qiskit-terra/issues/607 """ - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2) circ1 = QuantumCircuit(qr) circ1.cx(qr[0], qr[1]) diff --git a/test/python/compiler/test_disassembler.py b/test/python/compiler/test_disassembler.py index e5d0fd73a79c..0525b54e2107 100644 --- a/test/python/compiler/test_disassembler.py +++ b/test/python/compiler/test_disassembler.py @@ -457,7 +457,8 @@ class TestPulseScheduleDisassembler(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.backend_config = self.backend.configuration() self.backend_config.parametric_pulses = ["constant", "gaussian", "gaussian_square", "drag"] diff --git a/test/python/compiler/test_scheduler.py b/test/python/compiler/test_scheduler.py index 62e0af596ba4..ad9b14b24c4a 100644 --- a/test/python/compiler/test_scheduler.py +++ b/test/python/compiler/test_scheduler.py @@ -15,7 +15,7 @@ from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.providers.fake_provider import FakeOpenPulse3Q +from qiskit.providers.fake_provider import FakeOpenPulse3Q, GenericBackendV2 from qiskit.compiler.scheduler import schedule from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -37,9 +37,9 @@ def setUp(self): self.circ2.cx(qr2[0], qr2[1]) self.circ2.measure(qr2, cr2) - self.backend = FakeOpenPulse3Q() - self.backend_config = self.backend.configuration() - self.num_qubits = self.backend_config.n_qubits + self.backend = GenericBackendV2( + 3, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) def test_instruction_map_and_backend_not_supplied(self): """Test instruction map and backend not supplied.""" @@ -51,6 +51,8 @@ def test_instruction_map_and_backend_not_supplied(self): def test_instruction_map_and_backend_defaults_unavailable(self): """Test backend defaults unavailable when backend is provided, but instruction map is not.""" + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse3Q() self.backend._defaults = None with self.assertRaisesRegex( QiskitError, r"The backend defaults are unavailable. The backend may not support pulse." diff --git a/test/python/compiler/test_sequencer.py b/test/python/compiler/test_sequencer.py index 771e854effab..e32d6370367b 100644 --- a/test/python/compiler/test_sequencer.py +++ b/test/python/compiler/test_sequencer.py @@ -13,6 +13,8 @@ # pylint: disable=missing-function-docstring """Tests basic functionality of the sequence function""" +# TODO with the removal of pulses, this file can be removed too. + import unittest from qiskit import QuantumCircuit, pulse @@ -27,7 +29,8 @@ class TestSequence(QiskitTestCase): def setUp(self): super().setUp() - self.backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + self.backend = Fake127QPulseV1() self.backend.configuration().timing_constraints = {} def test_sequence_empty(self): diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 472fc732ca9a..46acff9d40d6 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -96,7 +96,7 @@ from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order -from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP +from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, MUMBAI_CMAP, TOKYO_CMAP class CustomCX(Gate): @@ -170,7 +170,7 @@ def test_num_processes_kwarg_concurrent_default(self, num_processes): qc.h(0) qc.cx(0, 1) qc.measure_all() - target = GenericBackendV2(num_qubits=27).target + target = GenericBackendV2(num_qubits=27, seed=42).target res = transpile([qc] * 3, target=target, num_processes=num_processes) self.assertIsInstance(res, list) for circ in res: @@ -269,7 +269,7 @@ def test_transpile_non_adjacent_layout(self): circuit.cx(qr[2], qr[3]) backend = GenericBackendV2( - num_qubits=15, basis_gates=["ecr", "id", "rz", "sx", "x"], coupling_map=cmap + num_qubits=15, basis_gates=["ecr", "id", "rz", "sx", "x"], coupling_map=cmap, seed=42 ) initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] @@ -313,7 +313,7 @@ def test_already_mapped_1(self): See: https://github.com/Qiskit/qiskit-terra/issues/342 """ - backend = GenericBackendV2(num_qubits=16, coupling_map=RUESCHLIKON_CMAP) + backend = GenericBackendV2(num_qubits=16, coupling_map=RUESCHLIKON_CMAP, seed=42) coupling_map = backend.coupling_map basis_gates = backend.operation_names @@ -598,7 +598,7 @@ def test_transpile_singleton(self): def test_mapping_correction(self): """Test mapping works in previous failed case.""" - backend = GenericBackendV2(num_qubits=12) + backend = GenericBackendV2(num_qubits=12, seed=42) qr = QuantumRegister(name="qr", size=11) cr = ClassicalRegister(name="qc", size=11) circuit = QuantumCircuit(qr, cr) @@ -718,7 +718,7 @@ def test_transpiler_layout_from_intlist(self): def test_mapping_multi_qreg(self): """Test mapping works for multiple qregs.""" - backend = GenericBackendV2(num_qubits=8) + backend = GenericBackendV2(num_qubits=8, seed=42) qr = QuantumRegister(3, name="qr") qr2 = QuantumRegister(1, name="qr2") qr3 = QuantumRegister(4, name="qr3") @@ -735,7 +735,7 @@ def test_mapping_multi_qreg(self): def test_transpile_circuits_diff_registers(self): """Transpile list of circuits with different qreg names.""" - backend = GenericBackendV2(num_qubits=4) + backend = GenericBackendV2(num_qubits=4, seed=42) circuits = [] for _ in range(2): qr = QuantumRegister(2) @@ -751,7 +751,7 @@ def test_transpile_circuits_diff_registers(self): def test_wrong_initial_layout(self): """Test transpile with a bad initial layout.""" - backend = GenericBackendV2(num_qubits=4) + backend = GenericBackendV2(num_qubits=4, seed=42) qubit_reg = QuantumRegister(2, name="q") clbit_reg = ClassicalRegister(2, name="c") @@ -790,7 +790,7 @@ def test_parameterized_circuit_for_device(self): theta = Parameter("theta") qc.p(theta, qr[0]) - backend = GenericBackendV2(num_qubits=4) + backend = GenericBackendV2(num_qubits=4, seed=42) transpiled_qc = transpile( qc, @@ -830,7 +830,7 @@ def test_parameter_expression_circuit_for_device(self): square = theta * theta qc.rz(square, qr[0]) - backend = GenericBackendV2(num_qubits=4) + backend = GenericBackendV2(num_qubits=4, seed=42) transpiled_qc = transpile( qc, backend=backend, @@ -863,7 +863,7 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "example.qasm")) layout = Layout.generate_trivial_layout(*circ.qregs) coupling_map = [] - for node1, node2 in GenericBackendV2(num_qubits=16).coupling_map: + for node1, node2 in GenericBackendV2(num_qubits=16, seed=42).coupling_map: coupling_map.append([node1, node2]) coupling_map.append([node2, node1]) @@ -922,7 +922,7 @@ def test_pass_manager_empty(self): def test_move_measurements(self): """Measurements applied AFTER swap mapping.""" - cmap = GenericBackendV2(num_qubits=16).coupling_map + cmap = GenericBackendV2(num_qubits=16, seed=42).coupling_map qasm_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm") circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "move_measurements.qasm")) @@ -966,7 +966,7 @@ def test_initialize_FakeMelbourne(self): qc = QuantumCircuit(qr) qc.initialize(desired_vector, [qr[0], qr[1], qr[2]]) - out = transpile(qc, backend=GenericBackendV2(num_qubits=4)) + out = transpile(qc, backend=GenericBackendV2(num_qubits=4, seed=42)) out_dag = circuit_to_dag(out) reset_nodes = out_dag.named_nodes("reset") @@ -1285,7 +1285,7 @@ def test_transpiled_custom_gates_calibration(self): transpiled_circuit = transpile( circ, - backend=GenericBackendV2(num_qubits=4), + backend=GenericBackendV2(num_qubits=4, seed=42), layout_method="trivial", seed_transpiler=42, ) @@ -1522,8 +1522,15 @@ def test_scheduling_timing_constraints(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" - backend_v1 = Fake27QPulseV1() - backend_v2 = BackendV2Converter(backend_v1) + with self.assertWarns(DeprecationWarning): + backend_v1 = Fake27QPulseV1() + backend_v2 = GenericBackendV2( + num_qubits=27, + calibrate_instructions=True, + control_flow=True, + coupling_map=MUMBAI_CMAP, + seed=42, + ) # the original timing constraints are granularity = min_length = 16 timing_constraints = TimingConstraints(granularity=32, min_length=64) error_msgs = { @@ -1557,7 +1564,8 @@ def test_scheduling_instruction_constraints(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" - backend_v1 = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend_v1 = Fake27QPulseV1() backend_v2 = BackendV2Converter(backend_v1) qc = QuantumCircuit(2) qc.h(0) @@ -1582,7 +1590,8 @@ def test_scheduling_dt_constraints(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" - backend_v1 = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend_v1 = Fake27QPulseV1() backend_v2 = BackendV2Converter(backend_v1) qc = QuantumCircuit(1, 1) qc.x(0) @@ -1602,7 +1611,8 @@ def test_backend_props_constraints(self): """Test that loose transpile constraints work with both BackendV1 and BackendV2.""" - backend_v1 = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend_v1 = Fake20QV1() backend_v2 = BackendV2Converter(backend_v1) qr1 = QuantumRegister(3, "qr1") qr2 = QuantumRegister(2, "qr2") @@ -2081,6 +2091,21 @@ def test_transpile_annotated_ops(self, opt_level): self.assertEqual(Operator(qc), Operator(transpiled)) self.assertEqual(Operator(qc), Operator(expected)) + @combine(opt_level=[0, 1, 2, 3]) + def test_transpile_annotated_ops_with_backend_v1(self, opt_level): + """Test transpilation of circuits with annotated operations given a backend. + Remove once Fake20QV1 is removed.""" + qc = QuantumCircuit(3) + qc.append(AnnotatedOperation(SGate(), InverseModifier()), [0]) + qc.append(AnnotatedOperation(XGate(), ControlModifier(1)), [1, 2]) + qc.append(AnnotatedOperation(HGate(), PowerModifier(3)), [2]) + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() + transpiled = transpile( + qc, optimization_level=opt_level, backend=backend, seed_transpiler=42 + ) + self.assertLessEqual(set(transpiled.count_ops().keys()), {"u1", "u2", "u3", "cx"}) + @combine(opt_level=[0, 1, 2, 3]) def test_transpile_annotated_ops_with_backend(self, opt_level): """Test transpilation of circuits with annotated operations given a backend.""" @@ -2088,7 +2113,12 @@ def test_transpile_annotated_ops_with_backend(self, opt_level): qc.append(AnnotatedOperation(SGate(), InverseModifier()), [0]) qc.append(AnnotatedOperation(XGate(), ControlModifier(1)), [1, 2]) qc.append(AnnotatedOperation(HGate(), PowerModifier(3)), [2]) - backend = Fake20QV1() + + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + ) transpiled = transpile( qc, optimization_level=opt_level, backend=backend, seed_transpiler=42 ) @@ -2396,9 +2426,32 @@ def test_qpy_roundtrip_standalone_var_target(self, optimization_level): @data(0, 1, 2, 3) def test_qasm3_output(self, optimization_level): """Test that the output of a transpiled circuit can be dumped into OpenQASM 3.""" + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + ) + transpiled = transpile( self._regular_circuit(), - backend=Fake20QV1(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2022_10_17, + ) + # TODO: There's not a huge amount we can sensibly test for the output here until we can + # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump + # itself doesn't throw an error, though. + self.assertIsInstance(qasm3.dumps(transpiled).strip(), str) + + @data(0, 1, 2, 3) + def test_qasm3_output_v1(self, optimization_level): + """Test that the output of a transpiled circuit can be dumped into OpenQASM 3 (backend V1).""" + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() + + transpiled = transpile( + self._regular_circuit(), + backend=backend, optimization_level=optimization_level, seed_transpiler=2022_10_17, ) diff --git a/test/python/legacy_cmaps.py b/test/python/legacy_cmaps.py index 08d2f043ca4e..84f4be45128b 100644 --- a/test/python/legacy_cmaps.py +++ b/test/python/legacy_cmaps.py @@ -426,3 +426,151 @@ [64, 54], [64, 63], ] + +# 127 qubits +KYOTO_CMAP = [ + [0, 14], + [1, 0], + [1, 2], + [3, 2], + [4, 3], + [4, 5], + [6, 5], + [7, 6], + [8, 7], + [8, 9], + [8, 16], + [9, 10], + [11, 10], + [11, 12], + [12, 13], + [15, 4], + [16, 26], + [17, 12], + [17, 30], + [18, 14], + [18, 19], + [19, 20], + [21, 20], + [22, 15], + [22, 21], + [22, 23], + [23, 24], + [25, 24], + [25, 26], + [27, 26], + [27, 28], + [28, 29], + [28, 35], + [30, 29], + [30, 31], + [31, 32], + [32, 36], + [33, 20], + [33, 39], + [34, 24], + [34, 43], + [37, 38], + [38, 39], + [39, 40], + [40, 41], + [42, 41], + [43, 42], + [44, 43], + [44, 45], + [46, 45], + [47, 35], + [47, 46], + [48, 47], + [49, 48], + [49, 55], + [50, 49], + [50, 51], + [51, 36], + [52, 37], + [53, 41], + [53, 60], + [54, 45], + [54, 64], + [55, 68], + [56, 52], + [57, 56], + [57, 58], + [59, 58], + [59, 60], + [61, 60], + [62, 61], + [62, 63], + [63, 64], + [64, 65], + [65, 66], + [67, 66], + [67, 68], + [68, 69], + [70, 69], + [71, 58], + [72, 62], + [73, 66], + [73, 85], + [74, 70], + [75, 90], + [76, 75], + [76, 77], + [77, 71], + [77, 78], + [79, 78], + [79, 91], + [80, 79], + [81, 72], + [81, 80], + [82, 81], + [82, 83], + [83, 92], + [84, 83], + [84, 85], + [86, 85], + [87, 86], + [87, 93], + [88, 87], + [89, 74], + [89, 88], + [93, 106], + [94, 90], + [94, 95], + [96, 95], + [96, 97], + [96, 109], + [97, 98], + [98, 91], + [98, 99], + [99, 100], + [101, 100], + [101, 102], + [102, 92], + [102, 103], + [104, 103], + [104, 111], + [105, 104], + [105, 106], + [106, 107], + [107, 108], + [109, 114], + [110, 100], + [112, 108], + [113, 114], + [115, 114], + [116, 115], + [117, 116], + [118, 110], + [118, 117], + [119, 118], + [120, 119], + [121, 120], + [122, 111], + [122, 121], + [122, 123], + [124, 123], + [125, 124], + [125, 126], + [126, 112], +] diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 626ba7625bc9..80b471b66063 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -311,7 +311,7 @@ class FakeBackendLimitedCircuits(GenericBackendV2): def max_circuits(self): return 1 - backend = FakeBackendLimitedCircuits(num_qubits=5) + backend = FakeBackendLimitedCircuits(num_qubits=5, seed=42) backend.set_options(seed_simulator=123) qc = RealAmplitudes(num_qubits=2, reps=2) op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) @@ -326,8 +326,10 @@ def max_circuits(self): self.assertEqual(run_mock.call_count, 10) def test_job_size_limit_v1(self): - """Test BackendEstimator respects job size limit""" - backend = Fake7QPulseV1() + """Test BackendEstimator respects job size limit + REMOVE ONCE Fake7QPulseV1 GETS REMOVED""" + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() config = backend.configuration() config.max_experiments = 1 backend._configuration = config @@ -345,8 +347,10 @@ def test_job_size_limit_v1(self): self.assertEqual(run_mock.call_count, 10) def test_no_max_circuits(self): - """Test BackendEstimator works with BackendV1 and no max_experiments set.""" - backend = Fake7QPulseV1() + """Test BackendEstimator works with BackendV1 and no max_experiments set. + REMOVE ONCE Fake7QPulseV1 GETS REMOVED""" + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() config = backend.configuration() del config.max_experiments backend._configuration = config @@ -387,7 +391,9 @@ def callback(msg): bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) with self.assertWarns(DeprecationWarning): - estimator = BackendEstimator(backend=Fake7QPulseV1(), bound_pass_manager=bound_pass) + estimator = BackendEstimator( + backend=GenericBackendV2(num_qubits=5, seed=42), bound_pass_manager=bound_pass + ) _ = estimator.run(qc, op).result() expected = [ "bound_pass_manager", @@ -409,7 +415,8 @@ def callback(msg): # pylint: disable=function-redefined bound_pass = PassManager(bound_counter) with self.assertWarns(DeprecationWarning): estimator = BackendEstimator( - backend=Fake7QPulseV1(), bound_pass_manager=bound_pass + backend=GenericBackendV2(num_qubits=5, seed=42), + bound_pass_manager=bound_pass, ) _ = estimator.run([qc, qc], [op, op]).result() expected = [ diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 6728d57e3fdd..319fd846ee95 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -446,8 +446,9 @@ def max_circuits(self): self.assertEqual(run_mock.call_count, 10) def test_job_size_limit_backend_v1(self): - """Test BackendEstimatorV2 respects job size limit""" - backend = Fake7QPulseV1() + """Test BackendEstimatorV2 respects job size limit from BackendV1""" + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() config = backend.configuration() config.max_experiments = 1 backend._configuration = config diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 8bc5f76ed095..f0fdb4f07f8c 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -323,9 +323,9 @@ def max_circuits(self): qc2.x(0) qc2.measure_all() with self.assertWarns(DeprecationWarning): - sampler = BackendSampler(backend=FakeBackendLimitedCircuits(num_qubits=5)) - result = sampler.run([qc, qc2]).result() - self.assertIsInstance(result, SamplerResult) + sampler = BackendSampler(backend=FakeBackendLimitedCircuits(num_qubits=5, seed=42)) + result = sampler.run([qc, qc2]).result() + self.assertIsInstance(result, SamplerResult) self.assertEqual(len(result.quasi_dists), 2) self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1) @@ -333,10 +333,9 @@ def max_circuits(self): def test_primitive_job_size_limit_backend_v1(self): """Test primitive respects backend's job size limit.""" - backend = Fake7QPulseV1() - config = backend.configuration() - config.max_experiments = 1 - backend._configuration = config + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) @@ -374,14 +373,17 @@ def test_circuit_with_dynamic_circuit(self): def test_sequential_run(self): """Test sequential run.""" + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) qc2.x(0) qc2.measure_all() with self.assertWarns(DeprecationWarning): - sampler = BackendSampler(backend=Fake7QPulseV1()) - result = sampler.run([qc]).result() + sampler = BackendSampler(backend=backend) + result = sampler.run([qc]).result() self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1) result2 = sampler.run([qc2]).result() self.assertDictAlmostEqual(result2.quasi_dists[0], {1: 1}, 0.1) @@ -421,9 +423,12 @@ def callback(msg): bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) with self.assertWarns(DeprecationWarning): - sampler = BackendSampler(backend=Fake7QPulseV1(), bound_pass_manager=bound_pass) - _ = sampler.run([self._circuit[0]]).result() + sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) + _ = sampler.run([self._circuit[0]]).result() expected = [ "bound_pass_manager", ] @@ -442,9 +447,12 @@ def callback(msg): # pylint: disable=function-redefined bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) with self.assertWarns(DeprecationWarning): - sampler = BackendSampler(backend=Fake7QPulseV1(), bound_pass_manager=bound_pass) - _ = sampler.run([self._circuit[0], self._circuit[0]]).result() + sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) + _ = sampler.run([self._circuit[0], self._circuit[0]]).result() expected = [ "bound_pass_manager", "bound_pass_manager", diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index b03818846c82..632ed1984d2d 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -714,10 +714,9 @@ def max_circuits(self): def test_job_size_limit_backend_v1(self): """Test BackendSamplerV2 respects backend's job size limit.""" - backend = Fake7QPulseV1() - config = backend.configuration() - config.max_experiments = 1 - backend._configuration = config + backend = GenericBackendV2( + 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) diff --git a/test/python/primitives/test_primitive.py b/test/python/primitives/test_primitive.py index c2b6b8f14202..fb96081fa001 100644 --- a/test/python/primitives/test_primitive.py +++ b/test/python/primitives/test_primitive.py @@ -21,7 +21,7 @@ from qiskit.circuit.random import random_circuit from qiskit.primitives.base import validation from qiskit.primitives.utils import _circuit_key -from qiskit.providers.fake_provider import Fake20QV1 +from qiskit.providers.fake_provider import GenericBackendV2 from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -142,7 +142,11 @@ def test_with_scheduling(n): qc = QuantumCircuit(1) qc.x(0) qc.add_calibration("x", qubits=(0,), schedule=custom_gate) - return transpile(qc, Fake20QV1(), scheduling_method="alap", optimization_level=1) + + backend = GenericBackendV2( + num_qubits=2, basis_gates=["id", "u1", "u2", "u3", "cx"], seed=42 + ) + return transpile(qc, backend, scheduling_method="alap", optimization_level=1) keys = [_circuit_key(test_with_scheduling(i)) for i in range(1, 5)] self.assertEqual(len(keys), len(set(keys))) diff --git a/test/python/providers/fake_provider/test_fake_backends.py b/test/python/providers/fake_provider/test_fake_backends.py index c12be3584fc2..72c9675e1ce7 100644 --- a/test/python/providers/fake_provider/test_fake_backends.py +++ b/test/python/providers/fake_provider/test_fake_backends.py @@ -15,7 +15,7 @@ import unittest from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.utils import optionals from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -36,9 +36,9 @@ class FakeBackendsTest(QiskitTestCase): """fake backends test.""" @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def test_fake_backends_get_kwargs(self): + def test_fake_backends_get_kwargs_v1(self): """Fake backends honor kwargs passed.""" - backend = Fake5QV1() + backend = GenericBackendV2(num_qubits=5, seed=42) qc = QuantumCircuit(2) qc.x(range(0, 2)) @@ -57,6 +57,7 @@ def test_fake_backend_v2_noise_model_always_present(self): qc = QuantumCircuit(1) qc.x(0) qc.measure_all() + res = backend.run(qc, shots=1000).result().get_counts() # Assert noise was present and result wasn't ideal self.assertNotEqual(res, {"1": 1000}) diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index 70ac50f659c9..42f46b7d7851 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -35,17 +35,17 @@ def setUp(self): def test_supported_basis_gates(self): """Test that target raises error if basis_gate not in ``supported_names``.""" with self.assertRaises(QiskitError): - GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"]) + GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"], seed=42) def test_cx_1Q(self): """Test failing with a backend with single qubit but with a two-qubit basis gate""" with self.assertRaises(QiskitError): - GenericBackendV2(num_qubits=1, basis_gates=["cx", "id"]) + GenericBackendV2(num_qubits=1, basis_gates=["cx", "id"], seed=42) def test_ccx_2Q(self): """Test failing with a backend with two qubits but with a three-qubit basis gate""" with self.assertRaises(QiskitError): - GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"]) + GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"], seed=42) def test_calibration_no_noise_info(self): """Test failing with a backend with calibration and no noise info""" @@ -55,12 +55,13 @@ def test_calibration_no_noise_info(self): basis_gates=["ccx", "id"], calibrate_instructions=True, noise_info=False, + seed=42, ) def test_no_noise(self): """Test no noise info when parameter is false""" backend = GenericBackendV2( - num_qubits=5, coupling_map=CouplingMap.from_line(5), noise_info=False + num_qubits=5, coupling_map=CouplingMap.from_line(5), noise_info=False, seed=42 ) qc = QuantumCircuit(5) qc.h(0) @@ -75,7 +76,7 @@ def test_no_noise(self): def test_no_noise_fully_connected(self): """Test no noise info when parameter is false""" - backend = GenericBackendV2(num_qubits=5, noise_info=False) + backend = GenericBackendV2(num_qubits=5, noise_info=False, seed=42) qc = QuantumCircuit(5) qc.h(0) qc.cx(0, 1) @@ -94,6 +95,7 @@ def test_no_info(self): coupling_map=CouplingMap.from_line(5), noise_info=False, pulse_channels=False, + seed=42, ) qc = QuantumCircuit(5) qc.h(0) @@ -109,7 +111,7 @@ def test_no_info(self): def test_no_pulse_channels(self): """Test no/empty pulse channels when parameter is false""" backend = GenericBackendV2( - num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False + num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False, seed=42 ) qc = QuantumCircuit(5) qc.h(0) @@ -125,12 +127,12 @@ def test_no_pulse_channels(self): def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" - target = GenericBackendV2(num_qubits=8) + target = GenericBackendV2(num_qubits=8, seed=42) op_names = list(target.operation_names) op_names.sort() self.assertEqual(op_names, ["cx", "delay", "id", "measure", "reset", "rz", "sx", "x"]) - target = GenericBackendV2(num_qubits=8, basis_gates=["ecr", "id", "rz", "sx", "x"]) + target = GenericBackendV2(num_qubits=8, basis_gates=["ecr", "id", "rz", "sx", "x"], seed=42) op_names = list(target.operation_names) op_names.sort() self.assertEqual(op_names, ["delay", "ecr", "id", "measure", "reset", "rz", "sx", "x"]) @@ -138,7 +140,7 @@ def test_operation_names(self): def test_incompatible_coupling_map(self): """Test that the size of the coupling map must match num_qubits.""" with self.assertRaises(QiskitError): - GenericBackendV2(num_qubits=5, coupling_map=self.cmap) + GenericBackendV2(num_qubits=5, coupling_map=self.cmap, seed=42) def test_control_flow_operation_names(self): """Test that control flow instructions are added to the target if control_flow is True.""" @@ -147,6 +149,7 @@ def test_control_flow_operation_names(self): basis_gates=["ecr", "id", "rz", "sx", "x"], coupling_map=self.cmap, control_flow=True, + seed=42, ).target op_names = list(target.operation_names) op_names.sort() @@ -176,7 +179,7 @@ def test_default_coupling_map(self): (1, 3), (3, 1), (1, 4), (4, 1), (2, 3), (3, 2), (2, 4), (4, 2), (3, 4), (4, 3)] # fmt: on self.assertEqual( - list(GenericBackendV2(num_qubits=5).coupling_map.get_edges()), + list(GenericBackendV2(num_qubits=5, seed=42).coupling_map.get_edges()), reference_cmap, ) @@ -191,7 +194,7 @@ def test_run(self): qc.cx(qr[0], qr[k]) qc.measure(qr, cr) - backend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"]) + backend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"], seed=42) tqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=42) result = backend.run(tqc, seed_simulator=42, shots=1000).result() counts = result.get_counts() @@ -214,7 +217,7 @@ def test_duration_defaults(self): "rxx": (7.992e-08, 8.99988e-07), } for _ in range(20): - target = GenericBackendV2(num_qubits=2, basis_gates=basis_gates).target + target = GenericBackendV2(num_qubits=2, basis_gates=basis_gates, seed=42).target for inst in target: for qargs in target.qargs_for_operation_name(inst): duration = target[inst][qargs].duration diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 40924e240826..df5f7b9abdea 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -41,7 +41,7 @@ class TestBackendV2(QiskitTestCase): def setUp(self): super().setUp() - self.backend = GenericBackendV2(num_qubits=2, seed=42, basis_gates=["rx", "u"]) + self.backend = GenericBackendV2(num_qubits=2, basis_gates=["rx", "u"], seed=42) cx_props = { (0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115), } @@ -127,9 +127,9 @@ def test_transpile(self, opt_level): ) def test_5q_ghz(self, opt_level, gate, bidirectional): if bidirectional: - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) else: - backend = GenericBackendV2(num_qubits=5, coupling_map=TENERIFE_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=TENERIFE_CMAP, seed=42) qc = QuantumCircuit(5) qc.h(0) getattr(qc, gate)(0, 1) @@ -207,7 +207,7 @@ def test_transpile_mumbai_target(self): @data(0, 1, 2, 3, 4) def test_drive_channel(self, qubit): """Test getting drive channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) chan = backend.drive_channel(qubit) ref = channels.DriveChannel(qubit) self.assertEqual(chan, ref) @@ -215,7 +215,7 @@ def test_drive_channel(self, qubit): @data(0, 1, 2, 3, 4) def test_measure_channel(self, qubit): """Test getting measure channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) chan = backend.measure_channel(qubit) ref = channels.MeasureChannel(qubit) self.assertEqual(chan, ref) @@ -223,7 +223,7 @@ def test_measure_channel(self, qubit): @data(0, 1, 2, 3, 4) def test_acquire_channel(self, qubit): """Test getting acquire channel with qubit index.""" - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) chan = backend.acquire_channel(qubit) ref = channels.AcquireChannel(qubit) self.assertEqual(chan, ref) @@ -241,7 +241,7 @@ def test_control_channel(self, qubits): (1, 0): 1, (0, 1): 0, } - backend = GenericBackendV2(num_qubits=5, coupling_map=BOGOTA_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=BOGOTA_CMAP, seed=42) chan = backend.control_channel(qubits)[0] ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) self.assertEqual(chan, ref) diff --git a/test/python/providers/test_backendconfiguration.py b/test/python/providers/test_backendconfiguration.py index 5ba727fd660d..82bbd1c6847f 100644 --- a/test/python/providers/test_backendconfiguration.py +++ b/test/python/providers/test_backendconfiguration.py @@ -12,6 +12,9 @@ """ Test that the PulseBackendConfiguration methods work as expected with a mocked Pulse backend. """ +# TODO the full file can be removed once BackendV1 is removed, since it is the +# only one with backend.configuration() + import collections import copy @@ -26,7 +29,8 @@ class TestBackendConfiguration(QiskitTestCase): def setUp(self): super().setUp() - backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() self.config = backend.configuration() def test_simple_config(self): @@ -60,7 +64,8 @@ def test_hamiltonian(self): {k: var * 1e-9 for k, var in ref_vars.items()}, ) # 3Q doesn't offer a hamiltonian -- test that we get a reasonable response - backend_3q = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend_3q = FakeOpenPulse3Q() self.assertEqual(backend_3q.configuration().hamiltonian, None) def test_get_channels(self): @@ -80,7 +85,8 @@ def test_get_channel_qubits(self): """Test to get all qubits operated on a given channel.""" self.assertEqual(self.config.get_channel_qubits(channel=DriveChannel(0)), [0]) self.assertEqual(self.config.get_channel_qubits(channel=ControlChannel(0)), [0, 1]) - backend_3q = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend_3q = FakeOpenPulse3Q() self.assertEqual(backend_3q.configuration().get_channel_qubits(ControlChannel(2)), [2, 1]) self.assertEqual(backend_3q.configuration().get_channel_qubits(ControlChannel(1)), [1, 0]) with self.assertRaises(BackendConfigurationError): @@ -107,7 +113,8 @@ def test_get_qubit_channels(self): ], ) ) - backend_3q = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend_3q = FakeOpenPulse3Q() self.assertTrue( self._test_lists_equal( actual=backend_3q.configuration().get_qubit_channels(1), @@ -178,7 +185,8 @@ def test_deepcopy(self): def test_u_channel_lo_scale(self): """Ensure that u_channel_lo scale is a complex number""" - valencia_conf = Fake27QPulseV1().configuration() + with self.assertWarns(DeprecationWarning): + valencia_conf = Fake27QPulseV1().configuration() self.assertTrue(isinstance(valencia_conf.u_channel_lo[0][0].scale, complex)) def test_processor_type(self): diff --git a/test/python/providers/test_backendproperties.py b/test/python/providers/test_backendproperties.py index b1455bd9a320..ac024973133e 100644 --- a/test/python/providers/test_backendproperties.py +++ b/test/python/providers/test_backendproperties.py @@ -22,12 +22,13 @@ class BackendpropertiesTestCase(QiskitTestCase): """Test usability methods of backend.properties().""" - backend = Fake5QV1() - backend_name = "fake_ourense" + # TODO the full file can be removed once BackendV1 is removed, since it is the + # only one with backend.properties() def setUp(self): super().setUp() - self.backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + self.backend = Fake5QV1() self.properties = self.backend.properties() self.ref_gate = next( g for g in self.backend.configuration().basis_gates if g not in ["id", "rz"] diff --git a/test/python/providers/test_backendstatus.py b/test/python/providers/test_backendstatus.py index 8372108408ac..2cfa31791de3 100644 --- a/test/python/providers/test_backendstatus.py +++ b/test/python/providers/test_backendstatus.py @@ -35,7 +35,8 @@ def test_repr(self): def test_fake_backend_status(self): """Test backend status for one of the fake backends""" - fake_backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + fake_backend = Fake5QV1() backend_status = fake_backend.status() self.assertIsInstance(backend_status, BackendStatus) diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 1a1c55d1ee2d..d743c4be5c53 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -78,12 +78,13 @@ from qiskit.transpiler.coupling import CouplingMap from test.utils.base import QiskitTestCase # pylint: disable=wrong-import-order -BACKENDS = [Fake5QV1(), Fake20QV1(), Fake7QPulseV1(), Fake27QPulseV1(), Fake127QPulseV1()] +with warnings.catch_warnings(): + BACKENDS = [Fake5QV1(), Fake20QV1(), Fake7QPulseV1(), Fake27QPulseV1(), Fake127QPulseV1()] BACKENDS_V2 = [] for n in [5, 7, 16, 20, 27, 65, 127]: cmap = CouplingMap.from_ring(n) - BACKENDS_V2.append(GenericBackendV2(num_qubits=n, coupling_map=cmap)) + BACKENDS_V2.append(GenericBackendV2(num_qubits=n, coupling_map=cmap, seed=42)) @ddt @@ -189,7 +190,7 @@ def test_to_dict_configuration(self, backend): self.assertIsInstance(configuration.to_dict(), dict) - @data(BACKENDS) + @data(*BACKENDS) def test_defaults_to_dict(self, backend): if hasattr(backend, "defaults"): defaults = backend.defaults() @@ -206,7 +207,8 @@ def test_defaults_to_dict(self, backend): self.skipTest(f"Backend {backend} does not have defaults") def test_delay_circuit(self): - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.configuration().timing_constraints = { "acquire_alignment": 1, "granularity": 1, @@ -223,7 +225,8 @@ def test_delay_circuit(self): @data(0, 1, 2, 3) def test_converter(self, opt_level): - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() backend_v2 = BackendV2Converter(backend) self.assertIsInstance(backend_v2, BackendV2) res = transpile(self.circuit, backend_v2, optimization_level=opt_level) @@ -237,7 +240,8 @@ def test_converter(self, opt_level): self.assertEqual(max_count, "11") def test_converter_delay_circuit(self): - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.configuration().timing_constraints = { "acquire_alignment": 1, "granularity": 1, @@ -256,8 +260,8 @@ def test_converter_delay_circuit(self): def test_converter_with_missing_gate_property(self): """Test converting to V2 model with irregular backend data.""" - backend = FakeOpenPulse2Q() - + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() # The backend includes pulse calibration definition for U2, but its property is gone. # Note that u2 is a basis gate of this device. # Since gate property is not provided, the gate broadcasts to all qubits as ideal instruction. @@ -268,9 +272,11 @@ def test_converter_with_missing_gate_property(self): self.assertDictEqual(backend_v2.target["u2"], {None: None}) def test_non_cx_tests(self): - backend = GenericBackendV2(num_qubits=5, basis_gates=["cz", "x", "sx", "id", "rz"]) + backend = GenericBackendV2(num_qubits=5, basis_gates=["cz", "x", "sx", "id", "rz"], seed=42) self.assertIsInstance(backend.target.operation_from_name("cz"), CZGate) - backend = GenericBackendV2(num_qubits=5, basis_gates=["ecr", "x", "sx", "id", "rz"]) + backend = GenericBackendV2( + num_qubits=5, basis_gates=["ecr", "x", "sx", "id", "rz"], seed=42 + ) self.assertIsInstance(backend.target.operation_from_name("ecr"), ECRGate) @unittest.skipUnless(optionals.HAS_AER, "Aer required for this test") @@ -521,7 +527,8 @@ def __init__(self, num_ctrl_qubits, ctrl_state=None): def test_filter_faulty_qubits_backend_v2_converter(self): """Test faulty qubits in v2 conversion.""" - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() # Get properties dict to make it easier to work with the properties API # is difficult to edit because of the multiple layers of nesting and # different object types @@ -542,7 +549,8 @@ def test_filter_faulty_qubits_backend_v2_converter(self): def test_filter_faulty_qubits_backend_v2_converter_with_delay(self): """Test faulty qubits in v2 conversion.""" - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() # Get properties dict to make it easier to work with the properties API # is difficult to edit because of the multiple layers of nesting and # different object types @@ -584,14 +592,16 @@ def test_backend_v2_converter_without_delay(self): (4, 2), (4, 3), } - - backend = BackendV2Converter(backend=Fake5QV1(), filter_faulty=True, add_delay=False) + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + backend = BackendV2Converter(backend=backend, filter_faulty=True, add_delay=False) self.assertEqual(backend.target.qargs, expected) def test_backend_v2_converter_with_meaningless_gate_config(self): """Test backend with broken gate config can be converted only with properties data.""" - backend_v1 = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend_v1 = Fake5QV1() backend_v1.configuration().gates = [ GateConfig(name="NotValidGate", parameters=[], qasm_def="not_valid_gate") ] @@ -608,7 +618,8 @@ def test_backend_v2_converter_with_meaningless_gate_config(self): def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): """Test faulty gates and qubits.""" - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() # Get properties dict to make it easier to work with the properties API # is difficult to edit because of the multiple layers of nesting and # different object types @@ -651,7 +662,8 @@ def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): def test_filter_faulty_gates_v2_converter(self): """Test just faulty gates in conversion.""" - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() # Get properties dict to make it easier to work with the properties API # is difficult to edit because of the multiple layers of nesting and # different object types @@ -685,14 +697,16 @@ def test_filter_faulty_gates_v2_converter(self): def test_filter_faulty_no_faults_v2_converter(self): """Test that faulty qubit filtering does nothing with all operational qubits and gates.""" - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() v2_backend = BackendV2Converter(backend, filter_faulty=True) for i in range(v2_backend.num_qubits): self.assertIn((i,), v2_backend.target.qargs) @data(0, 1, 2, 3) def test_faulty_full_path_transpile_connected_cmap(self, opt_level): - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() non_operational_gate = { "date": datetime.datetime.now(datetime.timezone.utc), "name": "operational", @@ -715,7 +729,8 @@ def test_faulty_full_path_transpile_connected_cmap(self, opt_level): self.assertNotIn((0, 1), connections) def test_convert_to_target_control_flow(self): - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() properties = backend.properties() configuration = backend.configuration() configuration.supported_instructions = [ @@ -739,7 +754,8 @@ def test_convert_to_target_control_flow(self): self.assertTrue(target.instruction_supported("switch_case", ())) def test_convert_unrelated_supported_instructions(self): - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() properties = backend.properties() configuration = backend.configuration() configuration.supported_instructions = [ diff --git a/test/python/providers/test_pulse_defaults.py b/test/python/providers/test_pulse_defaults.py index 33fe9ecbad65..18f849255917 100644 --- a/test/python/providers/test_pulse_defaults.py +++ b/test/python/providers/test_pulse_defaults.py @@ -17,7 +17,7 @@ import numpy as np -from qiskit.providers.fake_provider import FakeOpenPulse2Q +from qiskit.providers.fake_provider import FakeOpenPulse2Q, GenericBackendV2 from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -26,8 +26,13 @@ class TestPulseDefaults(QiskitTestCase): def setUp(self): super().setUp() - self.defs = FakeOpenPulse2Q().defaults() - self.inst_map = self.defs.instruction_schedule_map + with self.assertWarns(DeprecationWarning): + # BackendV2 does not have defaults + self.defs = FakeOpenPulse2Q().defaults() + backend = GenericBackendV2( + 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) + self.inst_map = backend.instruction_schedule_map def test_buffer(self): """Test getting the buffer value.""" diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index 3af2d0b510d1..c6a2c2384f64 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -28,7 +28,8 @@ class BaseTestBlock(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.test_waveform0 = pulse.Constant(100, 0.1) self.test_waveform1 = pulse.Constant(200, 0.1) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 1dc295a02be1..563b83345496 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -30,7 +30,8 @@ class TestBuilder(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.configuration = self.backend.configuration() self.defaults = self.backend.defaults() self.inst_map = self.defaults.instruction_schedule_map @@ -689,7 +690,8 @@ def test_measure_all(self): self.assertScheduleEqual(schedule, reference) - backend = Fake127QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() num_qubits = backend.configuration().num_qubits with pulse.build(backend) as schedule: regs = pulse.measure_all() diff --git a/test/python/pulse/test_builder_v2.py b/test/python/pulse/test_builder_v2.py index 843c8a8447fe..79d3ac5b6020 100644 --- a/test/python/pulse/test_builder_v2.py +++ b/test/python/pulse/test_builder_v2.py @@ -32,9 +32,7 @@ class TestBuilderV2(QiskitTestCase): def setUp(self): super().setUp() self.backend = GenericBackendV2( - num_qubits=27, - coupling_map=MUMBAI_CMAP, - calibrate_instructions=True, + num_qubits=27, coupling_map=MUMBAI_CMAP, calibrate_instructions=True, seed=42 ) def assertScheduleEqual(self, program, target): diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index 3ef2ccaa38e5..d1610d7ebcc0 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -100,7 +100,9 @@ def test_has(self): def test_has_from_mock(self): """Test `has` and `assert_has` from mock data.""" - inst_map = FakeOpenPulse2Q().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() + inst_map = backend.defaults().instruction_schedule_map self.assertTrue(inst_map.has("u1", [0])) self.assertTrue(inst_map.has("cx", (0, 1))) self.assertTrue(inst_map.has("u3", 0)) @@ -229,7 +231,9 @@ def test_has_gate(self): def test_has_from_mock_gate(self): """Test `has` and `assert_has` from mock data.""" - inst_map = FakeOpenPulse2Q().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() + inst_map = backend.defaults().instruction_schedule_map self.assertTrue(inst_map.has(U1Gate(0), [0])) self.assertTrue(inst_map.has(CXGate(), (0, 1))) self.assertTrue(inst_map.has(U3Gate(0, 0, 0), 0)) @@ -531,14 +535,18 @@ def test_callable_sched1(par_b): def test_two_instmaps_equal(self): """Test eq method when two instmaps are identical.""" - instmap1 = Fake7QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() + instmap1 = backend.defaults().instruction_schedule_map instmap2 = copy.deepcopy(instmap1) self.assertEqual(instmap1, instmap2) def test_two_instmaps_different(self): """Test eq method when two instmaps are not identical.""" - instmap1 = Fake7QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() + instmap1 = backend.defaults().instruction_schedule_map instmap2 = copy.deepcopy(instmap1) # override one of instruction @@ -548,7 +556,9 @@ def test_two_instmaps_different(self): def test_instmap_picklable(self): """Test if instmap can be pickled.""" - instmap = Fake7QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() + instmap = backend.defaults().instruction_schedule_map ser_obj = pickle.dumps(instmap) deser_instmap = pickle.loads(ser_obj) @@ -562,7 +572,9 @@ def test_instmap_picklable_with_arguments(self): in which arguments are provided by users in the form of python dict key object that is not picklable. """ - instmap = Fake7QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() + instmap = backend.defaults().instruction_schedule_map param1 = Parameter("P1") param2 = Parameter("P2") @@ -582,14 +594,18 @@ def test_instmap_picklable_with_arguments(self): def test_check_backend_provider_cals(self): """Test if schedules provided by backend provider is distinguishable.""" - instmap = FakeOpenPulse2Q().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() + instmap = backend.defaults().instruction_schedule_map publisher = instmap.get("u1", (0,), P0=0).metadata["publisher"] self.assertEqual(publisher, CalibrationPublisher.BACKEND_PROVIDER) def test_check_user_cals(self): """Test if schedules provided by user is distinguishable.""" - instmap = FakeOpenPulse2Q().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() + instmap = backend.defaults().instruction_schedule_map test_u1 = Schedule() test_u1 += ShiftPhase(Parameter("P0"), DriveChannel(0)) @@ -601,7 +617,8 @@ def test_check_user_cals(self): def test_has_custom_gate(self): """Test method to check custom gate.""" - backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse2Q() instmap = backend.defaults().instruction_schedule_map self.assertFalse(instmap.has_custom_gate()) diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py index 05937c88982d..6d87320cf480 100644 --- a/test/python/pulse/test_macros.py +++ b/test/python/pulse/test_macros.py @@ -33,12 +33,15 @@ class TestMeasure(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() + self.backend_v1 = Fake27QPulseV1() + self.inst_map = self.backend.defaults().instruction_schedule_map - self.backend_v1 = Fake27QPulseV1() self.backend_v2 = GenericBackendV2( num_qubits=27, calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + seed=42, ) def test_measure(self): @@ -171,7 +174,9 @@ def test_output_with_measure_v1_and_measure_v2_sched_with_qubit_mem_slots(self): def test_output_with_measure_v1_and_measure_v2_sched_with_meas_map(self): """Test make outputs of measure_v1 and measure_v2 with custom meas_map as list and dict consistent.""" - num_qubits_list_measure_v1 = list(range(Fake27QPulseV1().configuration().num_qubits)) + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() + num_qubits_list_measure_v1 = list(range(backend.configuration().num_qubits)) num_qubits_list_measure_v2 = list(range(self.backend_v2.num_qubits)) sched_with_meas_map_list_v1 = macros.measure( qubits=[0], backend=self.backend_v1, meas_map=[num_qubits_list_measure_v1] @@ -210,11 +215,13 @@ class TestMeasureAll(QiskitTestCase): def setUp(self): super().setUp() - self.backend_v1 = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend_v1 = FakeOpenPulse2Q() self.inst_map = self.backend_v1.defaults().instruction_schedule_map self.backend_v2 = GenericBackendV2( num_qubits=2, calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + seed=42, ) def test_measure_all(self): diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index 40d637887ed0..5e5676e7c2d9 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -61,7 +61,8 @@ def linear(duration, slope, intercept): return slope * x + intercept self.linear = linear - self.config = FakeOpenPulse2Q().configuration() + with self.assertWarns(DeprecationWarning): + self.config = FakeOpenPulse2Q().configuration() class TestScheduleBuilding(BaseTestSchedule): diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index 5918a6e6993d..c16405cff4d1 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -46,7 +46,8 @@ class TestAlignMeasures(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.config = self.backend.configuration() self.inst_map = self.backend.defaults().instruction_schedule_map self.short_pulse = pulse.Waveform( @@ -202,7 +203,8 @@ class TestAddImplicitAcquires(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.config = self.backend.configuration() self.short_pulse = pulse.Waveform( samples=np.array([0.02739068], dtype=np.complex128), name="p0" diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index a050a70cac5f..8f0cffb36a5f 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -61,8 +61,10 @@ class TestCalibrationPasses(QpyCircuitTestCase): def setUp(self): super().setUp() - # This backend provides CX(0,1) with native ECR direction. - self.inst_map = Fake27QPulseV1().defaults().instruction_schedule_map + # TODO remove context once https://github.com/Qiskit/qiskit/issues/12759 is fixed + with self.assertWarns(DeprecationWarning): + # This backend provides CX(0,1) with native ECR direction. + self.inst_map = Fake27QPulseV1().defaults().instruction_schedule_map @data(0.1, 0.7, 1.5) def test_rzx_calibration(self, angle): @@ -112,7 +114,7 @@ def test_transpile_layout(self, opt_level): qc.h(0) qc.cx(0, 1) qc.measure_all() - backend = GenericBackendV2(num_qubits=127) + backend = GenericBackendV2(num_qubits=127, seed=42) tqc = transpile(qc, backend, optimization_level=opt_level) self.assert_roundtrip_equal(tqc) @@ -126,7 +128,7 @@ def test_transpile_with_routing(self, opt_level): qc.cx(0, 3) qc.cx(0, 4) qc.measure_all() - backend = GenericBackendV2(num_qubits=127) + backend = GenericBackendV2(num_qubits=127, seed=42) tqc = transpile(qc, backend, optimization_level=opt_level) self.assert_roundtrip_equal(tqc) @@ -137,7 +139,7 @@ def test_transpile_layout_explicit_None_final_layout(self, opt_level): qc.h(0) qc.cx(0, 1) qc.measure_all() - backend = GenericBackendV2(num_qubits=127) + backend = GenericBackendV2(num_qubits=127, seed=42) tqc = transpile(qc, backend, optimization_level=opt_level) tqc.layout.final_layout = None self.assert_roundtrip_equal(tqc) @@ -201,7 +203,7 @@ def test_custom_register_name(self, opt_level): qc.cx(0, 3) qc.cx(0, 4) qc.measure_all() - backend = GenericBackendV2(num_qubits=127) + backend = GenericBackendV2(num_qubits=127, seed=42) tqc = transpile(qc, backend, optimization_level=opt_level) self.assert_roundtrip_equal(tqc) @@ -213,7 +215,7 @@ def test_no_register(self, opt_level): qc.h(0) qc.cx(0, 1) qc.measure_all() - backend = GenericBackendV2(num_qubits=127) + backend = GenericBackendV2(num_qubits=127, seed=42) tqc = transpile(qc, backend, optimization_level=opt_level) # Manually validate to deal with qubit equality needing exact objects qpy_file = io.BytesIO() diff --git a/test/python/result/test_mitigators.py b/test/python/result/test_mitigators.py index 3b3e83bce009..8dcdedc433fe 100644 --- a/test/python/result/test_mitigators.py +++ b/test/python/result/test_mitigators.py @@ -119,8 +119,11 @@ def counts_data(circuit, assignment_matrices, shots=1024): def test_mitigation_improvement(self): """Test whether readout mitigation led to more accurate results""" shots = 1024 - assignment_matrices = self.assignment_matrices() - num_qubits = len(assignment_matrices) + with self.assertWarns(DeprecationWarning): + # TODO self.assignment_matrices calls LocalReadoutMitigator, + # which only supports BackendV1 at the moment: + # https://github.com/Qiskit/qiskit/issues/12832 + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices) circuit, circuit_name, num_qubits = self.ghz_3_circuit() counts_ideal, counts_noise, probs_noise = self.counts_data( @@ -156,7 +159,8 @@ def test_expectation_improvement(self): """Test whether readout mitigation led to more accurate results and that its standard deviation is increased""" shots = 1024 - assignment_matrices = self.assignment_matrices() + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices) num_qubits = len(assignment_matrices) diagonals = [] @@ -198,7 +202,8 @@ def test_expectation_improvement(self): def test_clbits_parameter(self): """Test whether the clbits parameter is handled correctly""" shots = 10000 - assignment_matrices = self.assignment_matrices() + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) @@ -233,7 +238,8 @@ def test_clbits_parameter(self): def test_qubits_parameter(self): """Test whether the qubits parameter is handled correctly""" shots = 10000 - assignment_matrices = self.assignment_matrices() + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) @@ -281,7 +287,8 @@ def test_qubits_parameter(self): def test_repeated_qubits_parameter(self): """Tests the order of mitigated qubits.""" shots = 10000 - assignment_matrices = self.assignment_matrices() + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices, qubits=[0, 1, 2]) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) @@ -319,7 +326,8 @@ def test_qubits_subset_parameter(self): """Tests mitigation on a subset of the initial set of qubits.""" shots = 10000 - assignment_matrices = self.assignment_matrices() + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() mitigators = self.mitigators(assignment_matrices, qubits=[2, 4, 6]) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) @@ -364,7 +372,8 @@ def test_qubits_subset_parameter(self): def test_from_backend(self): """Test whether a local mitigator can be created directly from backend properties""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() num_qubits = len(backend.properties().qubits) probs = TestReadoutMitigation.rng.random((num_qubits, 2)) for qubit_idx, qubit_prop in enumerate(backend.properties().qubits): @@ -422,7 +431,9 @@ def test_error_handling(self): def test_expectation_value_endian(self): """Test that endian for expval is little.""" - mitigators = self.mitigators(self.assignment_matrices()) + with self.assertWarns(DeprecationWarning): + assignment_matrices = self.assignment_matrices() + mitigators = self.mitigators(assignment_matrices) counts = Counts({"10": 3, "11": 24, "00": 74, "01": 923}) for mitigator in mitigators: expval, _ = mitigator.expectation_value(counts, diagonal="IZ", qubits=[0, 1]) @@ -449,7 +460,9 @@ class TestLocalReadoutMitigation(QiskitTestCase): def test_assignment_matrix(self): """Tests that the local mitigator generates the full assignment matrix correctly""" qubits = [7, 2, 3] - assignment_matrices = LocalReadoutMitigator(backend=Fake5QV1())._assignment_mats[0:3] + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + assignment_matrices = LocalReadoutMitigator(backend=backend)._assignment_mats[0:3] expected_assignment_matrix = np.kron( np.kron(assignment_matrices[2], assignment_matrices[1]), assignment_matrices[0] ) diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py index 16ec00927c67..947a255ddf21 100644 --- a/test/python/scheduler/test_basic_scheduler.py +++ b/test/python/scheduler/test_basic_scheduler.py @@ -46,14 +46,16 @@ class TestBasicSchedule(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() + with self.assertWarns(DeprecationWarning): + self.backend = FakeOpenPulse2Q() self.inst_map = self.backend.defaults().instruction_schedule_map def test_unavailable_defaults(self): """Test backend with unavailable defaults.""" qr = QuantumRegister(1) qc = QuantumCircuit(qr) - backend = FakeBackend(None) + with self.assertWarns(DeprecationWarning): + backend = FakeBackend(None) backend.defaults = backend.configuration self.assertRaises(QiskitError, lambda: schedule(qc, backend)) @@ -252,7 +254,8 @@ def test_3q_schedule(self): # ┌──────┴───┴──────┐ └───────────────┘ ┌─┴─┐┌─────────────────┐ # q0_2: ┤ U2(0.778,0.122) ├───────────────────┤ X ├┤ U2(0.778,0.122) ├ # └─────────────────┘ └───┘└─────────────────┘ - backend = FakeOpenPulse3Q() + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse3Q() inst_map = backend.defaults().instruction_schedule_map q = QuantumRegister(3) c = ClassicalRegister(3) @@ -416,9 +419,13 @@ def test_subset_calibrated_measurements(self): meas_scheds.append(meas) qc.add_calibration("measure", [qubit], meas) - meas = macros.measure([1], FakeOpenPulse3Q()) + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse3Q() + meas = macros.measure([1], backend) meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - sched = schedule(qc, FakeOpenPulse3Q()) + with self.assertWarns(DeprecationWarning): + backend = FakeOpenPulse3Q() + sched = schedule(qc, backend) expected = Schedule(meas_scheds[0], meas_scheds[1], meas) self.assertEqual(sched.instructions, expected.instructions) @@ -509,7 +516,7 @@ class TestBasicScheduleV2(QiskitTestCase): def setUp(self): super().setUp() - self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True) + self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) self.inst_map = self.backend.instruction_schedule_map # self.pulse_2_samples is the pulse sequence used to calibrate "measure" in # GenericBackendV2. See class construction for more details. diff --git a/test/python/transpiler/test_1q.py b/test/python/transpiler/test_1q.py index 50bdc7b24643..13379f410c34 100644 --- a/test/python/transpiler/test_1q.py +++ b/test/python/transpiler/test_1q.py @@ -16,12 +16,16 @@ from qiskit import QuantumCircuit from qiskit.compiler import transpile -from qiskit.providers.fake_provider import Fake1Q +from qiskit.providers.fake_provider import Fake1Q, GenericBackendV2 from qiskit.providers.basic_provider import BasicSimulator from qiskit.transpiler import TranspilerError from test import combine # pylint: disable=wrong-import-order from test import QiskitTestCase # pylint: disable=wrong-import-order +Fake1QV2 = GenericBackendV2( + num_qubits=1, basis_gates=["u1", "u2", "u3"], coupling_map=None, dtm=1.3333, seed=42 +) + def emptycircuit(): """Empty circuit""" @@ -45,31 +49,54 @@ class Test1QFailing(QiskitTestCase): circuit=[circuit_3516], level=[0, 1, 2, 3], dsc="Transpiling {circuit.__name__} at level {level} should fail", - name="{circuit.__name__}_level{level}_fail", + name="{circuit.__name__}_level{level}_fail_v1", + ) + def test(self, circuit, level): + """All the levels with all the 1Q backendV1""" + with self.assertRaises(TranspilerError): + with self.assertWarns(DeprecationWarning): + transpile(circuit(), backend=Fake1Q(), optimization_level=level, seed_transpiler=42) + + +@ddt +class Test1QV2Failing(QiskitTestCase): + """1QV2 tests that should fail.""" + + @combine( + circuit=[circuit_3516], + level=[0, 1, 2, 3], + dsc="Transpiling {circuit.__name__} at level {level} should fail", + name="{circuit.__name__}_level{level}_fail_v2", ) def test(self, circuit, level): - """All the levels with all the 1Q backend""" + """All the levels with all the 1Q backendV2""" with self.assertRaises(TranspilerError): - transpile(circuit(), backend=Fake1Q(), optimization_level=level, seed_transpiler=42) + transpile(circuit(), backend=Fake1QV2, optimization_level=level, seed_transpiler=42) @ddt class Test1QWorking(QiskitTestCase): - """1Q tests that should work.""" + """1QV1 tests that should work.""" @combine( circuit=[emptycircuit], level=[0, 1, 2, 3], dsc="Transpiling {circuit.__name__} at level {level} should work", - name="{circuit.__name__}_level{level}_valid", + name="{circuit.__name__}_level{level}_valid_v1", ) def test_device(self, circuit, level): - """All the levels with all the 1Q backend""" - result = transpile( - circuit(), backend=Fake1Q(), optimization_level=level, seed_transpiler=42 - ) + """All the levels with all the 1Q backendV1""" + with self.assertWarns(DeprecationWarning): + result = transpile( + circuit(), backend=Fake1Q(), optimization_level=level, seed_transpiler=42 + ) self.assertIsInstance(result, QuantumCircuit) + +@ddt +class TestBasicSimulatorWorking(QiskitTestCase): + """All the levels with a simulator backend""" + @combine( circuit=[circuit_3516], level=[0, 1, 2, 3], @@ -81,3 +108,21 @@ def test_simulator(self, circuit, level): backend = BasicSimulator() result = transpile(circuit(), backend=backend, optimization_level=level, seed_transpiler=42) self.assertIsInstance(result, QuantumCircuit) + + +@ddt +class Test1QV2Working(QiskitTestCase): + """1QV2 tests that should work.""" + + @combine( + circuit=[emptycircuit], + level=[0, 1, 2, 3], + dsc="Transpiling {circuit.__name__} at level {level} should work", + name="{circuit.__name__}_level{level}_valid_v2", + ) + def test_device(self, circuit, level): + """All the levels with all the 1Q backendV2""" + result = transpile( + circuit(), backend=Fake1QV2, optimization_level=level, seed_transpiler=42 + ) + self.assertIsInstance(result, QuantumCircuit) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index bc64d954a429..cb7293049ef5 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -266,7 +266,8 @@ def build_reverse( @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) def test_rzx_calibration_cr_pulse_stretch(self, theta: float): """Test that cross resonance pulse durations are computed correctly.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map cr_schedule = inst_map.get("cx", (0, 1)) with builder.build() as test_sched: @@ -279,7 +280,8 @@ def test_rzx_calibration_cr_pulse_stretch(self, theta: float): @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): """Test that rotary pulse durations are computed correctly.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map cr_schedule = inst_map.get("cx", (0, 1)) with builder.build() as test_sched: @@ -296,8 +298,8 @@ def test_raise(self): qc = circuit.QuantumCircuit(2) qc.rzx(theta, 0, 1) dag = circuit_to_dag(qc) - - backend = Fake7QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake7QPulseV1() # The error is raised when calibrations in multi-qubit # gates are not detected. # We force this by removing the 'cx' entries from the @@ -322,7 +324,8 @@ def test_ecr_cx_forward(self): qc = circuit.QuantumCircuit(2) qc.rzx(theta, 0, 1) - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map _pass = RZXCalibrationBuilder(inst_map) test_qc = PassManager(_pass).run(qc) @@ -347,7 +350,8 @@ def test_ecr_cx_reverse(self): qc = circuit.QuantumCircuit(2) qc.rzx(theta, 1, 0) - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map _pass = RZXCalibrationBuilder(inst_map) test_qc = PassManager(_pass).run(qc) @@ -434,7 +438,8 @@ def test_ecr_cx_forward(self): qc = circuit.QuantumCircuit(2) qc.rzx(theta, 0, 1) - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map _pass = RZXCalibrationBuilderNoEcho(inst_map) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index c4efffc977f4..8f876dd261d7 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -32,7 +32,12 @@ class TestEchoRZXWeylDecomposition(QiskitTestCase): def setUp(self): super().setUp() - self.backend = Fake27QPulseV1() + # TODO once https://github.com/Qiskit/qiskit/issues/12759 is fixed, replace with + # backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True, + # control_flow=True, seed=42) + # self.inst_map = backend.instruction_schedule_map + with self.assertWarns(DeprecationWarning): + self.backend = Fake27QPulseV1() self.inst_map = self.backend.defaults().instruction_schedule_map def assertRZXgates(self, unitary_circuit, after): diff --git a/test/python/transpiler/test_instruction_durations.py b/test/python/transpiler/test_instruction_durations.py index ba37f6cd65e4..de68fbadf86a 100644 --- a/test/python/transpiler/test_instruction_durations.py +++ b/test/python/transpiler/test_instruction_durations.py @@ -36,14 +36,18 @@ def test_fail_if_invalid_dict_is_supplied_when_construction(self): InstructionDurations(invalid_dic) def test_from_backend_for_backend_with_dt(self): - backend = Fake27QPulseV1() + # Remove context once https://github.com/Qiskit/qiskit/issues/12760 is fixed + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() gate = self._find_gate_with_length(backend) durations = InstructionDurations.from_backend(backend) self.assertGreater(durations.dt, 0) self.assertGreater(durations.get(gate, 0), 0) def test_from_backend_for_backend_without_dt(self): - backend = Fake27QPulseV1() + # Remove context once https://github.com/Qiskit/qiskit/issues/12760 is fixed + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() delattr(backend.configuration(), "dt") gate = self._find_gate_with_length(backend) durations = InstructionDurations.from_backend(backend) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index 01ec7ebf133a..ebac6a410b7f 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -19,6 +19,7 @@ from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.passmanager_config import PassManagerConfig from test import QiskitTestCase # pylint: disable=wrong-import-order +from ..legacy_cmaps import ALMADEN_CMAP class TestPassManagerConfig(QiskitTestCase): @@ -30,7 +31,8 @@ def test_config_from_backend(self): `Fake27QPulseV1` is used in this testcase. This backend has `defaults` attribute that contains an instruction schedule map. """ - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() config = PassManagerConfig.from_backend(backend) self.assertEqual(config.basis_gates, backend.configuration().basis_gates) self.assertEqual(config.inst_map, backend.defaults().instruction_schedule_map) @@ -40,7 +42,7 @@ def test_config_from_backend(self): def test_config_from_backend_v2(self): """Test from_backend() with a BackendV2 instance.""" - backend = GenericBackendV2(num_qubits=27) + backend = GenericBackendV2(num_qubits=27, seed=42) config = PassManagerConfig.from_backend(backend) self.assertEqual(config.basis_gates, backend.operation_names) self.assertEqual(config.inst_map, backend.instruction_schedule_map) @@ -51,16 +53,19 @@ def test_invalid_backend(self): with self.assertRaises(AttributeError): PassManagerConfig.from_backend(Backend()) - def test_from_backend_and_user(self): + def test_from_backend_and_user_v1(self): """Test from_backend() with a backend and user options. `FakeMelbourne` is used in this testcase. This backend does not have `defaults` attribute and thus not provide an instruction schedule map. + + REMOVE AFTER Fake20QV1 GETS REMOVED. """ qr = QuantumRegister(4, "qr") initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() config = PassManagerConfig.from_backend( backend, basis_gates=["user_gate"], initial_layout=initial_layout ) @@ -72,9 +77,35 @@ def test_from_backend_and_user(self): ) self.assertEqual(config.initial_layout, initial_layout) + def test_from_backend_and_user(self): + """Test from_backend() with a backend and user options. + + `FakeMelbourne` is used in this testcase. This backend does not have + `defaults` attribute and thus not provide an instruction schedule map. + """ + qr = QuantumRegister(4, "qr") + initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] + + backend = GenericBackendV2( + num_qubits=20, + coupling_map=ALMADEN_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + calibrate_instructions=None, + seed=42, + ) + config = PassManagerConfig.from_backend( + backend, basis_gates=["user_gate"], initial_layout=initial_layout + ) + self.assertEqual(config.basis_gates, ["user_gate"]) + self.assertNotEqual(config.basis_gates, backend.operation_names) + self.assertEqual(config.inst_map.instructions, []) + self.assertEqual(str(config.coupling_map), str(CouplingMap(backend.coupling_map))) + self.assertEqual(config.initial_layout, initial_layout) + def test_from_backendv1_inst_map_is_none(self): """Test that from_backend() works with backend that has defaults defined as None.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults = lambda: None config = PassManagerConfig.from_backend(backend) self.assertIsInstance(config, PassManagerConfig) @@ -82,8 +113,9 @@ def test_from_backendv1_inst_map_is_none(self): def test_invalid_user_option(self): """Test from_backend() with an invalid user option.""" + backend = GenericBackendV2(num_qubits=20, seed=42) with self.assertRaises(TypeError): - PassManagerConfig.from_backend(Fake20QV1(), invalid_option=None) + PassManagerConfig.from_backend(backend, invalid_option=None) def test_str(self): """Test string output.""" diff --git a/test/python/transpiler/test_passmanager_run.py b/test/python/transpiler/test_passmanager_run.py index caf6af2aaf67..3da6a042564c 100644 --- a/test/python/transpiler/test_passmanager_run.py +++ b/test/python/transpiler/test_passmanager_run.py @@ -15,9 +15,10 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit.library import CXGate from qiskit.transpiler.preset_passmanagers import level_1_pass_manager -from qiskit.providers.fake_provider import Fake20QV1 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import Layout, PassManager from qiskit.transpiler.passmanager_config import PassManagerConfig +from ..legacy_cmaps import ALMADEN_CMAP from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -83,12 +84,14 @@ def test_default_pass_manager_single(self): circuit.cx(qr[1], qr[2]) circuit.cx(qr[2], qr[3]) - coupling_map = Fake20QV1().configuration().coupling_map + backend = GenericBackendV2( + num_qubits=20, coupling_map=ALMADEN_CMAP, basis_gates=["id", "u1", "u2", "u3", "cx"] + ) initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] pass_manager = level_1_pass_manager( PassManagerConfig.from_backend( - Fake20QV1(), + backend, initial_layout=Layout.from_qubit_list(initial_layout), seed_transpiler=42, ) @@ -100,7 +103,7 @@ def test_default_pass_manager_single(self): for instruction in new_circuit.data: if isinstance(instruction.operation, CXGate): - self.assertIn([bit_indices[x] for x in instruction.qubits], coupling_map) + self.assertIn([bit_indices[x] for x in instruction.qubits], ALMADEN_CMAP) def test_default_pass_manager_two(self): """Test default_pass_manager.run(circuitS). @@ -133,12 +136,63 @@ def test_default_pass_manager_two(self): circuit2.cx(qr[0], qr[1]) circuit2.cx(qr[2], qr[3]) - coupling_map = Fake20QV1().configuration().coupling_map + coupling_map = [ + [0, 1], + [1, 0], + [1, 2], + [1, 6], + [2, 1], + [2, 3], + [3, 2], + [3, 4], + [3, 8], + [4, 3], + [5, 6], + [5, 10], + [6, 1], + [6, 5], + [6, 7], + [7, 6], + [7, 8], + [7, 12], + [8, 3], + [8, 7], + [8, 9], + [9, 8], + [9, 14], + [10, 5], + [10, 11], + [11, 10], + [11, 12], + [11, 16], + [12, 7], + [12, 11], + [12, 13], + [13, 12], + [13, 14], + [13, 18], + [14, 9], + [14, 13], + [15, 16], + [16, 11], + [16, 15], + [16, 17], + [17, 16], + [17, 18], + [18, 13], + [18, 17], + [18, 19], + [19, 18], + ] initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] + backend = GenericBackendV2( + num_qubits=20, coupling_map=coupling_map, basis_gates=["id", "u1", "u2", "u3", "cx"] + ) + pass_manager = level_1_pass_manager( PassManagerConfig.from_backend( - Fake20QV1(), + backend=backend, initial_layout=Layout.from_qubit_list(initial_layout), seed_transpiler=42, ) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 949d754573c5..3d11a5d55e93 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -222,7 +222,9 @@ def test_alignment_constraints_called_with_by_default(self, level): circuit.h(q[0]) circuit.cz(q[0], q[1]) with unittest.mock.patch("qiskit.transpiler.passes.TimeUnitConversion.run") as mock: - transpile(circuit, backend=Fake20QV1(), optimization_level=level) + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() + transpile(circuit, backend=backend, optimization_level=level) mock.assert_not_called() @combine(level=[0, 1, 2, 3], name="level{level}") @@ -236,12 +238,15 @@ def test_alignment_constraints_called_with_delay_in_circuit(self, level): with unittest.mock.patch( "qiskit.transpiler.passes.TimeUnitConversion.run", return_value=circuit_to_dag(circuit) ) as mock: - transpile(circuit, backend=Fake20QV1(), optimization_level=level) + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() + transpile(circuit, backend=backend, optimization_level=level) mock.assert_called_once() def test_unroll_only_if_not_gates_in_basis(self): """Test that the list of passes _unroll only runs if a gate is not in the basis.""" - qcomp = Fake5QV1() + with self.assertWarns(DeprecationWarning): + qcomp = Fake5QV1() qv_circuit = QuantumVolume(3) gates_in_basis_true_count = 0 collect_2q_blocks_count = 0 @@ -782,7 +787,8 @@ def test_layout_2503(self, level): 19: ancilla[16], } - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() qc_b = transpile(qc, backend, initial_layout=initial_layout, optimization_level=level) self.assertEqual(qc_b._layout.initial_layout._p2v, final_layout) @@ -1038,7 +1044,8 @@ def test_trivial_layout(self, level): expected_layouts = [trivial_layout, trivial_layout] - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() result = transpile(qc, backend, optimization_level=level, seed_transpiler=42) self.assertEqual(result._layout.initial_layout._p2v, expected_layouts[level]) @@ -1071,7 +1078,8 @@ def test_initial_layout(self, level): 18: qr[9], } - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() result = transpile( qc, backend, optimization_level=level, initial_layout=initial_layout, seed_transpiler=42 ) @@ -1151,7 +1159,8 @@ def test_optimization_condition(self, level): cr = ClassicalRegister(1) qc = QuantumCircuit(qr, cr) qc.cx(0, 1).c_if(cr, 1) - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() circ = transpile(qc, backend, optimization_level=level) self.assertIsInstance(circ, QuantumCircuit) @@ -1215,8 +1224,9 @@ class TestGeneratePresetPassManagers(QiskitTestCase): @data(0, 1, 2, 3) def test_with_backend(self, optimization_level): """Test a passmanager is constructed when only a backend and optimization level.""" - target = Fake20QV1() - pm = generate_preset_pass_manager(optimization_level, target) + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() + pm = generate_preset_pass_manager(optimization_level, backend) self.assertIsInstance(pm, PassManager) def test_default_optimization_level(self): @@ -1587,7 +1597,8 @@ def test_invalid_methods_raise_on_control_flow(self, optimization_level): def test_unsupported_basis_gates_raise(self, optimization_level): """Test that trying to transpile a control-flow circuit for a backend that doesn't support the necessary operations in its `basis_gates` will raise a sensible error.""" - backend = Fake20QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake20QV1() qc = QuantumCircuit(1, 1) with qc.for_loop((0,)): diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index a11d4c4a6b53..539674609c26 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -56,7 +56,8 @@ def setUp(self): def test_transpile_with_bare_backend(self): """Test transpile without custom calibrations.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() # Remove timing constraints to avoid triggering # scheduling passes. backend.configuration().timing_constraints = {} @@ -96,7 +97,8 @@ def test_transpile_with_backend_target(self): def test_transpile_with_custom_basis_gate(self): """Test transpile with custom calibrations.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) # Remove timing constraints to avoid triggering @@ -122,10 +124,13 @@ def test_transpile_with_custom_basis_gate(self): def test_transpile_with_custom_basis_gate_in_target(self): """Test transpile with custom calibrations.""" + with self.assertWarns(DeprecationWarning): + backend_pulse = Fake27QPulseV1() target = GenericBackendV2( num_qubits=5, coupling_map=BOGOTA_CMAP, - calibrate_instructions=Fake27QPulseV1().defaults().instruction_schedule_map, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, + seed=42, ).target target["sx"][(0,)].calibration = self.custom_sx_q0 @@ -150,12 +155,15 @@ def test_transpile_with_custom_basis_gate_in_target(self): def test_transpile_with_instmap(self): """Test providing instruction schedule map.""" - instmap = Fake27QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) # Inst map is renewed - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() # Remove timing constraints to avoid triggering # scheduling passes. backend.configuration().timing_constraints = {} @@ -179,7 +187,8 @@ def test_transpile_with_instmap(self): def test_transpile_with_custom_gate(self): """Test providing non-basis gate.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) @@ -215,7 +224,8 @@ def test_transpile_with_custom_gate(self): def test_transpile_with_parameterized_custom_gate(self): """Test providing non-basis gate, which is kept parameterized throughout transpile.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) @@ -244,7 +254,8 @@ def test_transpile_with_parameterized_custom_gate(self): def test_transpile_with_multiple_circuits(self): """Test transpile with multiple circuits with custom gate.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) @@ -274,7 +285,8 @@ def test_transpile_with_multiple_circuits(self): def test_multiple_instructions_with_different_parameters(self): """Test adding many instruction with different parameter binding.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) @@ -308,7 +320,8 @@ def test_multiple_instructions_with_different_parameters(self): def test_transpile_with_different_qubit(self): """Test transpile with qubit without custom gate.""" - backend = Fake27QPulseV1() + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) # Remove timing constraints to avoid triggering # scheduling passes. @@ -329,16 +342,20 @@ def test_transpile_with_both_instmap_and_empty_target(self, opt_level): Test case from Qiskit/qiskit-terra/#9489 """ - instmap = Fake27QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) instmap.add("cx", (0, 1), self.custom_cx_q01) + with self.assertWarns(DeprecationWarning): + backend_pulse = Fake27QPulseV1() # This doesn't have custom schedule definition target = GenericBackendV2( num_qubits=5, coupling_map=BOGOTA_CMAP, - calibrate_instructions=Fake27QPulseV1().defaults().instruction_schedule_map, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, seed=42, ).target @@ -371,7 +388,10 @@ def test_transpile_with_instmap_with_v2backend(self, opt_level): Test case from Qiskit/qiskit-terra/#9489 """ - instmap = Fake27QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() + + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) instmap.add("cx", (0, 1), self.custom_cx_q01) @@ -380,9 +400,12 @@ def test_transpile_with_instmap_with_v2backend(self, opt_level): qc.append(random_unitary(4, seed=123), [0, 1]) qc.measure_all() + with self.assertWarns(DeprecationWarning): + backend_pulse = Fake27QPulseV1() + backend = GenericBackendV2( num_qubits=5, - calibrate_instructions=Fake27QPulseV1().defaults().instruction_schedule_map, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, seed=42, ) @@ -416,8 +439,9 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) """ with pulse.build(name="custom") as rabi12: pulse.play(pulse.Constant(100, 0.4), pulse.DriveChannel(0)) - - instmap = Fake27QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() + instmap = backend.defaults().instruction_schedule_map instmap.add("rabi12", (0,), rabi12) gate = circuit.Gate("rabi12", 1, []) @@ -457,7 +481,10 @@ def test_transpile_with_instmap_not_mutate_backend(self): ) original_sx0 = backend.target["sx"][(0,)].calibration - instmap = Fake27QPulseV1().defaults().instruction_schedule_map + with self.assertWarns(DeprecationWarning): + backend_pulse = Fake27QPulseV1() + + instmap = backend_pulse.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) qc = circuit.QuantumCircuit(1) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 4c09bf12efa8..42ae3691c3ca 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -195,8 +195,10 @@ def test_layout_with_classical_bits(self): rz(0) q4835[1]; """ ) + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() res = transpile( - qc, Fake27QPulseV1(), layout_method="sabre", seed_transpiler=1234, optimization_level=1 + qc, backend, layout_method="sabre", seed_transpiler=1234, optimization_level=1 ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout @@ -247,9 +249,11 @@ def test_layout_many_search_trials(self): barrier q18585[5],q18585[2],q18585[8],q18585[3],q18585[6]; """ ) + with self.assertWarns(DeprecationWarning): + backend = Fake27QPulseV1() res = transpile( qc, - Fake27QPulseV1(), + backend, layout_method="sabre", routing_method="stochastic", seed_transpiler=12345, diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index bf2ef5b1f9c4..6650ca27c6f4 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -25,7 +25,7 @@ from qiskit.circuit.random import random_circuit from qiskit.compiler.transpiler import transpile from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler.passes import SabreSwap, TrivialLayout, CheckMap from qiskit.transpiler import CouplingMap, Layout, PassManager, Target, TranspilerError from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit @@ -1327,11 +1327,15 @@ class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = Fake27QPulseV1() - cls.backend.configuration().coupling_map = MUMBAI_CMAP - cls.backend.configuration().basis_gates += ["for_loop", "while_loop", "if_else"] - cls.coupling_edge_set = {tuple(x) for x in cls.backend.configuration().coupling_map} - cls.basis_gates = set(cls.backend.configuration().basis_gates) + cls.backend = GenericBackendV2( + num_qubits=27, + calibrate_instructions=True, + control_flow=True, + coupling_map=MUMBAI_CMAP, + seed=42, + ) + cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} + cls.basis_gates = set(cls.backend.operation_names) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" diff --git a/test/python/transpiler/test_star_prerouting.py b/test/python/transpiler/test_star_prerouting.py index e244d97a45aa..fb67698300b4 100644 --- a/test/python/transpiler/test_star_prerouting.py +++ b/test/python/transpiler/test_star_prerouting.py @@ -21,10 +21,7 @@ from qiskit.circuit.library import QFT from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.converters import ( - circuit_to_dag, - dag_to_circuit, -) +from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.quantum_info import Operator from qiskit.transpiler.passes import VF2Layout, ApplyLayout, SabreSwap, SabreLayout from qiskit.transpiler.passes.routing.star_prerouting import StarPreRouting diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 8c96150ae8ff..4843bac8baf5 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -24,7 +24,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.transpiler.passes.utils import CheckMap from qiskit.circuit.random import random_circuit -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.compiler.transpiler import transpile from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT from qiskit.circuit.classical import expr, types @@ -1488,10 +1488,11 @@ class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = Fake27QPulseV1() - cls.backend.configuration().basis_gates += ["for_loop", "while_loop", "if_else"] - cls.coupling_edge_set = {tuple(x) for x in cls.backend.configuration().coupling_map} - cls.basis_gates = set(cls.backend.configuration().basis_gates) + cls.backend = GenericBackendV2( + num_qubits=27, calibrate_instructions=True, control_flow=True, seed=42 + ) + cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} + cls.basis_gates = set(cls.backend.operation_names) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index f63ed5061cc7..928cd09c8dd1 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1937,7 +1937,8 @@ def test_basis_gates_coupling_map(self): self.assertEqual({(0, 1), (1, 2), (2, 0)}, target["cx"].keys()) def test_properties(self): - fake_backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + fake_backend = Fake5QV1() config = fake_backend.configuration() properties = fake_backend.properties() target = Target.from_configuration( @@ -1950,7 +1951,8 @@ def test_properties(self): self.assertEqual(0, target["rz"][(0,)].duration) def test_properties_with_durations(self): - fake_backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + fake_backend = Fake5QV1() config = fake_backend.configuration() properties = fake_backend.properties() durations = InstructionDurations([("rz", 0, 0.5)], dt=1.0) @@ -1965,7 +1967,8 @@ def test_properties_with_durations(self): self.assertEqual(0.5, target["rz"][(0,)].duration) def test_inst_map(self): - fake_backend = Fake7QPulseV1() + with self.assertWarns(DeprecationWarning): + fake_backend = Fake7QPulseV1() config = fake_backend.configuration() properties = fake_backend.properties() defaults = fake_backend.defaults() @@ -1986,7 +1989,8 @@ def test_inst_map(self): self.assertEqual(target.acquire_alignment, constraints.acquire_alignment) def test_concurrent_measurements(self): - fake_backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + fake_backend = Fake5QV1() config = fake_backend.configuration() target = Target.from_configuration( basis_gates=config.basis_gates, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 0f4423426a8a..43acd4ef67af 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -148,7 +148,8 @@ def test_two_qubit_synthesis_to_directional_cx_from_gate_errors(self): """Verify two qubit unitaries are synthesized to match basis gates.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap(conf.coupling_map) @@ -182,7 +183,8 @@ def test_swap_synthesis_to_directional_cx(self): """Verify two qubit unitaries are synthesized to match basis gates.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap(conf.coupling_map) @@ -218,7 +220,8 @@ def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self): across multiple registers.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr0 = QuantumRegister(1) qr1 = QuantumRegister(1) @@ -253,7 +256,8 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self): """Verify natural cx direction is used when specified in coupling map.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -296,7 +300,8 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_none(se when natural_direction is None.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -339,7 +344,8 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_false(s when natural_direction is None.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -379,7 +385,8 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_false(s def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) qc = QuantumCircuit(qr) @@ -415,7 +422,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): def test_two_qubit_pulse_optimal_true_raises(self): """Verify raises if pulse optimal==True but cx is not in the backend basis.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() # this assumes iswawp pulse optimal decomposition doesn't exist conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] @@ -438,7 +446,8 @@ def test_two_qubit_pulse_optimal_true_raises(self): def test_two_qubit_natural_direction_true_duration_fallback(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" # this assumes iswawp pulse optimal decomposition doesn't exist - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() # conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) @@ -462,7 +471,8 @@ def test_two_qubit_natural_direction_true_duration_fallback(self): def test_two_qubit_natural_direction_true_gate_length_raises(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" # this assumes iswawp pulse optimal decomposition doesn't exist - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() for _, nduv in backend.properties()._gates["cx"].items(): nduv["gate_length"] = (4e-7, nduv["gate_length"][1]) @@ -485,7 +495,8 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" # this assumes iswawp pulse optimal decomposition doesn't exist - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -512,7 +523,8 @@ def test_two_qubit_pulse_optimal_none_no_raise(self): """Verify pulse optimal decomposition when pulse_optimize==None doesn't raise when pulse optimal decomposition unknown.""" # this assumes iswawp pulse optimal decomposition doesn't exist - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() conf = backend.configuration() conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) @@ -662,7 +674,8 @@ def test_coupling_map_unequal_durations(self, opt): qr = QuantumRegister(2) circ = QuantumCircuit(qr) circ.append(random_unitary(4, seed=1), [1, 0]) - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() tqc = transpile( circ, backend=backend, diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 716e49d35009..4e4844f5f701 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -629,7 +629,8 @@ def test_no_properties(self): def test_with_properties(self): """Test it finds the least noise perfect layout with no properties.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) @@ -643,7 +644,8 @@ def test_with_properties(self): def test_max_trials_exceeded(self): """Test it exits when max_trials is reached.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) @@ -663,7 +665,8 @@ def test_max_trials_exceeded(self): def test_time_limit_exceeded(self): """Test the pass stops after time_limit is reached.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) @@ -685,9 +688,11 @@ def test_time_limit_exceeded(self): self.assertEqual(set(property_set["layout"].get_physical_bits()), {2, 0}) - def test_reasonable_limits_for_simple_layouts(self): - """Test that the default trials is set to a reasonable number.""" - backend = Fake127QPulseV1() + def test_reasonable_limits_for_simple_layouts_v1(self): + """Test that the default trials is set to a reasonable number. + REMOVE ONCE Fake127QPulseV1 IS GONE""" + with self.assertWarns(DeprecationWarning): + backend = Fake127QPulseV1() qc = QuantumCircuit(5) qc.cx(2, 3) qc.cx(0, 1) @@ -704,9 +709,28 @@ def test_reasonable_limits_for_simple_layouts(self): ) self.assertEqual(set(property_set["layout"].get_physical_bits()), {57, 58, 61, 62, 0}) + def test_reasonable_limits_for_simple_layouts(self): + """Test that the default trials is set to a reasonable number.""" + backend = GenericBackendV2(27, calibrate_instructions=True, seed=42) + qc = QuantumCircuit(5) + qc.cx(2, 3) + qc.cx(0, 1) + + # Run without any limits set + vf2_pass = VF2Layout(target=backend.target, seed=42) + property_set = {} + with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: + vf2_pass(qc, property_set) + self.assertIn( + "DEBUG:qiskit.transpiler.passes.layout.vf2_layout:Trial 717 is >= configured max trials 717", + cm.output, + ) + self.assertEqual(set(property_set["layout"].get_physical_bits()), {16, 24, 6, 7, 0}) + def test_no_limits_with_negative(self): """Test that we're not enforcing a trial limit if set to negative.""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qc = QuantumCircuit(3) qc.h(0) cmap = CouplingMap(backend.configuration().coupling_map) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 3df2fefcc73b..e97ed279a8d8 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -102,8 +102,9 @@ def test_no_backend_properties(self): def test_empty_circuit(self): """Test no solution found for empty circuit""" + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qc = QuantumCircuit(2, 2) - backend = Fake5QV1() cmap = CouplingMap(backend.configuration().coupling_map) props = backend.properties() vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) @@ -128,9 +129,10 @@ def test_empty_circuit_v2(self): def test_skip_3q_circuit(self): """Test that the pass is a no-op on circuits with >2q gates.""" + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qc = QuantumCircuit(3) qc.ccx(0, 1, 2) - backend = Fake5QV1() cmap = CouplingMap(backend.configuration().coupling_map) props = backend.properties() vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) @@ -141,10 +143,11 @@ def test_skip_3q_circuit(self): def test_skip_3q_circuit_control_flow(self): """Test that the pass is a no-op on circuits with >2q gates.""" + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qc = QuantumCircuit(3) with qc.for_loop((1,)): qc.ccx(0, 1, 2) - backend = Fake5QV1() cmap = CouplingMap(backend.configuration().coupling_map) props = backend.properties() vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) @@ -182,7 +185,8 @@ def test_skip_3q_circuit_control_flow_v2(self): def test_best_mapping_ghz_state_full_device_multiple_qregs(self): """Test best mappings with multiple registers""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr_a = QuantumRegister(2) qr_b = QuantumRegister(3) qc = QuantumCircuit(qr_a, qr_b) @@ -207,7 +211,8 @@ def test_2q_circuit_5q_backend(self): 0 - 1 qr1 - qr0 """ - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) @@ -227,7 +232,8 @@ def test_2q_circuit_5q_backend_controlflow(self): 0 - 1 qr1 - qr0 """ - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() circuit = QuantumCircuit(2, 1) with circuit.for_loop((1,)): @@ -253,7 +259,8 @@ def test_2q_circuit_5q_backend_max_trials(self): qr1 - qr0 """ max_trials = 11 - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) @@ -564,8 +571,10 @@ def test_no_backend_properties(self): def test_empty_circuit(self): """Test no solution found for empty circuit""" + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + qc = QuantumCircuit(2, 2) - backend = Fake5QV1() cmap = CouplingMap(backend.configuration().coupling_map) props = backend.properties() vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) @@ -593,9 +602,11 @@ def test_empty_circuit_v2(self): def test_skip_3q_circuit(self): """Test that the pass is a no-op on circuits with >2q gates.""" + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + qc = QuantumCircuit(3) qc.ccx(0, 1, 2) - backend = Fake5QV1() cmap = CouplingMap(backend.configuration().coupling_map) props = backend.properties() vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) @@ -624,7 +635,8 @@ def test_skip_3q_circuit_v2(self): def test_best_mapping_ghz_state_full_device_multiple_qregs(self): """Test best mappings with multiple registers""" - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr_a = QuantumRegister(2) qr_b = QuantumRegister(3) qc = QuantumCircuit(qr_a, qr_b) @@ -651,7 +663,8 @@ def test_2q_circuit_5q_backend(self): 0 - 1 qr1 - qr0 """ - backend = Fake5QV1() + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index d459bd945046..dd69faac02cb 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -249,7 +249,7 @@ def test_warning_for_bad_justify_argument(self): error_message = re.escape( f"Setting QuantumCircuit.draw()’s or circuit_drawer()'s justify argument: {bad_arg}, to a " "value other than 'left', 'right', 'none' or None (='left'). Default 'left' will be used. " - "Support for invalid justify arguments is deprecated as of qiskit 1.2.0. Starting no " + "Support for invalid justify arguments is deprecated as of Qiskit 1.2.0. Starting no " "earlier than 3 months after the release date, invalid arguments will error.", ) diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index 5490648fd6bf..1be7ad7ce051 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -480,11 +480,15 @@ def test_partial_layout(self): """Tests partial_layout See: https://github.com/Qiskit/qiskit-terra/issues/4757""" filename = self._get_resource_path("test_latex_partial_layout.tex") + + with self.assertWarns(DeprecationWarning): + backend = Fake5QV1() + circuit = QuantumCircuit(3) circuit.h(1) transpiled = transpile( circuit, - backend=Fake5QV1(), + backend=backend, optimization_level=0, initial_layout=[1, 2, 0], seed_transpiler=0, diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index bf9b1ca80d79..fcac1e71c400 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -19,8 +19,6 @@ Fake5QV1, Fake20QV1, Fake7QPulseV1, - Fake27QPulseV1, - Fake127QPulseV1, GenericBackendV2, ) from qiskit.visualization import ( @@ -33,6 +31,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.transpiler.layout import Layout, TranspileLayout from .visualization import path_to_diagram_reference, QiskitVisualizationTestCase +from ..legacy_cmaps import KYOTO_CMAP, MUMBAI_CMAP if optionals.HAS_MATPLOTLIB: import matplotlib.pyplot as plt @@ -108,7 +107,11 @@ def test_plot_gate_map_no_backend(self): @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_backend_v1(self): """Test plotting error map with fake backend v1.""" - backend = Fake27QPulseV1() + backend = GenericBackendV2( + num_qubits=27, + pulse_channels=True, + coupling_map=MUMBAI_CMAP, + ) img_ref = path_to_diagram_reference("fake_27_q_error.png") fig = plot_error_map(backend) with BytesIO() as img_buffer: @@ -122,9 +125,11 @@ def test_plot_error_map_backend_v1(self): @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_backend_v2(self): """Test plotting error map with fake backend v2.""" + coupling_map = MUMBAI_CMAP backend = GenericBackendV2( num_qubits=27, - coupling_map=Fake27QPulseV1().configuration().coupling_map, + pulse_channels=True, + coupling_map=coupling_map, ) img_ref = path_to_diagram_reference("fake_27_q_v2_error.png") fig = plot_error_map(backend) @@ -139,7 +144,10 @@ def test_plot_error_map_backend_v2(self): @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_over_100_qubit(self): """Test plotting error map with large fake backend.""" - backend = Fake127QPulseV1() + coupling_map = KYOTO_CMAP + backend = GenericBackendV2( + num_qubits=127, coupling_map=coupling_map, pulse_channels=True, seed=42 + ) img_ref = path_to_diagram_reference("fake_127_q_error.png") fig = plot_error_map(backend) with BytesIO() as img_buffer: @@ -153,9 +161,294 @@ def test_plot_error_map_over_100_qubit(self): @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_over_100_qubit_backend_v2(self): """Test plotting error map with large fake backendv2.""" + coupling_map = [ + [0, 1], + [0, 14], + [1, 0], + [1, 2], + [2, 1], + [2, 3], + [3, 2], + [3, 4], + [4, 3], + [4, 5], + [4, 15], + [5, 4], + [5, 6], + [6, 5], + [6, 7], + [7, 6], + [7, 8], + [8, 7], + [8, 16], + [9, 10], + [10, 9], + [10, 11], + [11, 10], + [11, 12], + [12, 11], + [12, 13], + [12, 17], + [13, 12], + [14, 0], + [14, 18], + [15, 4], + [15, 22], + [16, 8], + [16, 26], + [17, 12], + [17, 30], + [18, 14], + [18, 19], + [19, 18], + [19, 20], + [20, 19], + [20, 21], + [20, 33], + [21, 20], + [21, 22], + [22, 15], + [22, 21], + [22, 23], + [23, 22], + [23, 24], + [24, 23], + [24, 25], + [24, 34], + [25, 24], + [25, 26], + [26, 16], + [26, 25], + [26, 27], + [27, 26], + [27, 28], + [28, 27], + [28, 29], + [28, 35], + [29, 28], + [29, 30], + [30, 17], + [30, 29], + [30, 31], + [31, 30], + [31, 32], + [32, 31], + [32, 36], + [33, 20], + [33, 39], + [34, 24], + [34, 43], + [35, 28], + [35, 47], + [36, 32], + [36, 51], + [37, 38], + [37, 52], + [38, 37], + [38, 39], + [39, 33], + [39, 38], + [39, 40], + [40, 39], + [40, 41], + [41, 40], + [41, 42], + [41, 53], + [42, 41], + [42, 43], + [43, 34], + [43, 42], + [43, 44], + [44, 43], + [44, 45], + [45, 44], + [45, 46], + [45, 54], + [46, 45], + [46, 47], + [47, 35], + [47, 46], + [47, 48], + [48, 47], + [48, 49], + [49, 48], + [49, 50], + [49, 55], + [50, 49], + [50, 51], + [51, 36], + [51, 50], + [52, 37], + [52, 56], + [53, 41], + [53, 60], + [54, 45], + [54, 64], + [55, 49], + [55, 68], + [56, 52], + [56, 57], + [57, 56], + [57, 58], + [58, 57], + [58, 59], + [58, 71], + [59, 58], + [59, 60], + [60, 53], + [60, 59], + [60, 61], + [61, 60], + [61, 62], + [62, 61], + [62, 63], + [62, 72], + [63, 62], + [63, 64], + [64, 54], + [64, 63], + [64, 65], + [65, 64], + [65, 66], + [66, 65], + [66, 67], + [66, 73], + [67, 66], + [67, 68], + [68, 55], + [68, 67], + [68, 69], + [69, 68], + [69, 70], + [70, 69], + [70, 74], + [71, 58], + [71, 77], + [72, 62], + [72, 81], + [73, 66], + [73, 85], + [74, 70], + [74, 89], + [75, 76], + [75, 90], + [76, 75], + [76, 77], + [77, 71], + [77, 76], + [77, 78], + [78, 77], + [78, 79], + [79, 78], + [79, 80], + [79, 91], + [80, 79], + [80, 81], + [81, 72], + [81, 80], + [81, 82], + [82, 81], + [82, 83], + [83, 82], + [83, 84], + [83, 92], + [84, 83], + [84, 85], + [85, 73], + [85, 84], + [85, 86], + [86, 85], + [86, 87], + [87, 86], + [87, 88], + [87, 93], + [88, 87], + [88, 89], + [89, 74], + [89, 88], + [90, 75], + [90, 94], + [91, 79], + [91, 98], + [92, 83], + [92, 102], + [93, 87], + [93, 106], + [94, 90], + [94, 95], + [95, 94], + [95, 96], + [96, 95], + [96, 97], + [96, 109], + [97, 96], + [97, 98], + [98, 91], + [98, 97], + [98, 99], + [99, 98], + [99, 100], + [100, 99], + [100, 101], + [100, 110], + [101, 100], + [101, 102], + [102, 92], + [102, 101], + [102, 103], + [103, 102], + [103, 104], + [104, 103], + [104, 105], + [104, 111], + [105, 104], + [105, 106], + [106, 93], + [106, 105], + [106, 107], + [107, 106], + [107, 108], + [108, 107], + [108, 112], + [109, 96], + [110, 100], + [110, 118], + [111, 104], + [111, 122], + [112, 108], + [112, 126], + [113, 114], + [114, 113], + [114, 115], + [115, 114], + [115, 116], + [116, 115], + [116, 117], + [117, 116], + [117, 118], + [118, 110], + [118, 117], + [118, 119], + [119, 118], + [119, 120], + [120, 119], + [120, 121], + [121, 120], + [121, 122], + [122, 111], + [122, 121], + [122, 123], + [123, 122], + [123, 124], + [124, 123], + [124, 125], + [125, 124], + [125, 126], + [126, 112], + [126, 125], + ] backend = GenericBackendV2( - num_qubits=127, - coupling_map=Fake127QPulseV1().configuration().coupling_map, + num_qubits=127, coupling_map=coupling_map, pulse_channels=True, seed=42 ) img_ref = path_to_diagram_reference("fake_127_q_v2_error.png") fig = plot_error_map(backend) From f8ac2ad22c1ada79e2c58ba1a584b95884692140 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 29 Jul 2024 08:50:08 -0400 Subject: [PATCH 10/27] Add config option to leverage all cores for sabre (#12780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add config option to leverage all cores for sabre By default when running sabre in parallel we use a fixed number of threads (depending on optimization level). This was a tradeoff made for having deterministic results across multiple systems with a fixed seed set. However when running qiskit on systems with a lot of CPUs available we're leaving potential performance on the table by not using all the available cores. This new flag lets users opt-in to running sabre with n trials for n CPUs to potentially get better output results from the transpiler, with minimal to no runtime overhead, at the cost of the results not necessarily being reproducible when run on a different computer. * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Rework logic to use the default if larger than CPU_COUNT This commit refactors the logic added in the previous commit to a single helper function. This reduces the code duplication and makes it easier to work with. While doing this the logic has been updated so that when the flag is set and the default number of trials is larger than the CPU_COUNT we use the default. This means the logic when the flag is set is to run `max(default_trials, CPU_COUNT)` which should better match user expectations around the flag. --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../preset_passmanagers/builtin_plugins.py | 70 ++++++++++++++----- qiskit/user_config.py | 9 +++ ...e-all-threads-option-ad4ff7a4d045cb2b.yaml | 26 +++++++ 3 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index f85b4d113c17..5e42c7ba3e3f 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -12,6 +12,8 @@ """Built-in transpiler stage plugins for preset pass managers.""" +import os + from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BasicSwap @@ -63,6 +65,10 @@ SXGate, SXdgGate, ) +from qiskit.utils.parallel import CPU_COUNT +from qiskit import user_config + +CONFIG = user_config.get_config() class DefaultInitPassManager(PassManagerStagePlugin): @@ -397,11 +403,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.initial_layout, ) if optimization_level == 0: + trial_count = _get_trial_count(5) routing_pass = SabreSwap( coupling_map_routing, heuristic="basic", seed=seed_transpiler, - trials=5, + trials=trial_count, ) return common.generate_routing_passmanager( routing_pass, @@ -411,11 +418,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 1: + trial_count = _get_trial_count(5) routing_pass = SabreSwap( coupling_map_routing, heuristic="decay", seed=seed_transpiler, - trials=5, + trials=trial_count, ) return common.generate_routing_passmanager( routing_pass, @@ -429,11 +437,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 2: + trial_count = _get_trial_count(20) + routing_pass = SabreSwap( coupling_map_routing, heuristic="decay", seed=seed_transpiler, - trials=10, + trials=trial_count, ) return common.generate_routing_passmanager( routing_pass, @@ -446,11 +456,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 3: + trial_count = _get_trial_count(20) routing_pass = SabreSwap( coupling_map_routing, heuristic="decay", seed=seed_transpiler, - trials=20, + trials=trial_count, ) return common.generate_routing_passmanager( routing_pass, @@ -737,12 +748,15 @@ def _swap_mapped(property_set): max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) layout.append(ConditionalController(choose_layout_1, condition=_layout_not_perfect)) + + trial_count = _get_trial_count(5) + choose_layout_2 = SabreLayout( coupling_map, max_iterations=2, seed=pass_manager_config.seed_transpiler, - swap_trials=5, - layout_trials=5, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) @@ -769,12 +783,15 @@ def _swap_mapped(property_set): layout.append( ConditionalController(choose_layout_0, condition=_choose_layout_condition) ) + + trial_count = _get_trial_count(20) + choose_layout_1 = SabreLayout( coupling_map, max_iterations=2, seed=pass_manager_config.seed_transpiler, - swap_trials=20, - layout_trials=20, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) @@ -801,12 +818,15 @@ def _swap_mapped(property_set): layout.append( ConditionalController(choose_layout_0, condition=_choose_layout_condition) ) + + trial_count = _get_trial_count(20) + choose_layout_1 = SabreLayout( coupling_map, max_iterations=4, seed=pass_manager_config.seed_transpiler, - swap_trials=20, - layout_trials=20, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) @@ -902,42 +922,50 @@ def _swap_mapped(property_set): layout = PassManager() layout.append(_given_layout) if optimization_level == 0: + trial_count = _get_trial_count(5) + layout_pass = SabreLayout( coupling_map, max_iterations=1, seed=pass_manager_config.seed_transpiler, - swap_trials=5, - layout_trials=5, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) elif optimization_level == 1: + trial_count = _get_trial_count(5) + layout_pass = SabreLayout( coupling_map, max_iterations=2, seed=pass_manager_config.seed_transpiler, - swap_trials=5, - layout_trials=5, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) elif optimization_level == 2: + trial_count = _get_trial_count(20) + layout_pass = SabreLayout( coupling_map, max_iterations=2, seed=pass_manager_config.seed_transpiler, - swap_trials=20, - layout_trials=20, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) elif optimization_level == 3: + trial_count = _get_trial_count(20) + layout_pass = SabreLayout( coupling_map, max_iterations=4, seed=pass_manager_config.seed_transpiler, - swap_trials=20, - layout_trials=20, + swap_trials=trial_count, + layout_trials=trial_count, skip_routing=pass_manager_config.routing_method is not None and pass_manager_config.routing_method != "sabre", ) @@ -957,3 +985,9 @@ def _swap_mapped(property_set): embed = common.generate_embed_passmanager(coupling_map) layout.append(ConditionalController(embed.to_flow_controller(), condition=_swap_mapped)) return layout + + +def _get_trial_count(default_trials=5): + if CONFIG.get("sabre_all_threads", None) or os.getenv("QISKIT_SABRE_ALL_THREADS"): + return max(CPU_COUNT, default_trials) + return default_trials diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 0ca52fc5c8c0..22d12406b348 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -35,6 +35,7 @@ class UserConfig: transpile_optimization_level = 1 parallel = False num_processes = 4 + sabre_all_threads = true """ @@ -168,6 +169,13 @@ def read_config_file(self): ) self.settings["num_processes"] = num_processes + # Parse sabre_all_threads + sabre_all_threads = self.config_parser.getboolean( + "default", "sabre_all_threads", fallback=None + ) + if sabre_all_threads is not None: + self.settings["sabre_all_threads"] = sabre_all_threads + def set_config(key, value, section=None, file_path=None): """Adds or modifies a user configuration @@ -208,6 +216,7 @@ def set_config(key, value, section=None, file_path=None): "transpile_optimization_level", "parallel", "num_processes", + "sabre_all_threads", } if section in [None, "default"]: diff --git a/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml b/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml new file mode 100644 index 000000000000..24b64a28703e --- /dev/null +++ b/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml @@ -0,0 +1,26 @@ +--- +features_transpiler: + - | + Added a new user config file option ``sabre_all_threads`` and a + corresponding environment variable ``QISKIT_SABRE_ALL_THREADS``. When this + flag is set the preset pass managers will run the :class:`.SabreLayout` + and :class:`.SabreSwap` transpiler passes using all the available + CPUs on the local system. Using this option is a tradeoff between + determinism of output between different computers and potentially better + output with fewer :class:`.SwapGate`\s. + + These transpiler passes run multiple random trials in parallel and pick + the output which results in the fewest :class:`.SwapGate`\s. As a rule of + thumb, if you run more trials, this provides the algorithm more opportunities + to find a better result. By default, the preset pass managers use a fixed + number of trials, in this release 5 trials for levels 0 and 1, and 20 + trials for levels 2 and 3, but these numbers may change in future releases + (and were different in historical releases). Using a fixed number of + trials results in deterministic results regardless of the local system, + because even with a fixed seed if you were to default to the number of + local CPUs available the results would different when running between + different computers. + + If the default number of trials for a given optimization level is higher + than the number of local CPUs it will use the optimization level default + which is higher. From bfd2eea4807a894fa229cb55b3ad221a50fd31ee Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:10:19 +0900 Subject: [PATCH 11/27] Make `BitArray.{slice_bits,slice_shots,__getitem__}` raise `IndexError` when indices are not valid (#12755) * Make BitArray.{slice_bits,slice_shots} raise IndexError when indices are out of bounds * update __getitem__ --- qiskit/primitives/containers/bit_array.py | 21 +++++++++++------- ...ray-slice-bits-shots-c9cb7e5d907722f5.yaml | 11 ++++++++++ .../primitives/containers/test_bit_array.py | 22 +++++++++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index 29ff3240f3bf..b458b3d49232 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -130,10 +130,15 @@ def __repr__(self): return f"BitArray({desc})" def __getitem__(self, indices): - if isinstance(indices, tuple) and len(indices) >= self.ndim + 2: - raise ValueError( - "BitArrays cannot be sliced along the bits axis, see slice_bits() instead." - ) + if isinstance(indices, tuple): + if len(indices) == self.ndim + 1: + raise IndexError( + "BitArray cannot be sliced along the shots axis, use slice_shots() instead." + ) + if len(indices) >= self.ndim + 2: + raise IndexError( + "BitArray cannot be sliced along the bits axis, use slice_bits() instead." + ) return BitArray(self._array[indices], self.num_bits) @property @@ -430,13 +435,13 @@ def slice_bits(self, indices: int | Sequence[int]) -> "BitArray": A bit array sliced along the bit axis. Raises: - ValueError: If there are any invalid indices of the bit axis. + IndexError: If there are any invalid indices of the bit axis. """ if isinstance(indices, int): indices = (indices,) for index in indices: if index < 0 or index >= self.num_bits: - raise ValueError( + raise IndexError( f"index {index} is out of bounds for the number of bits {self.num_bits}." ) # This implementation introduces a temporary 8x memory overhead due to bit @@ -457,13 +462,13 @@ def slice_shots(self, indices: int | Sequence[int]) -> "BitArray": A bit array sliced along the shots axis. Raises: - ValueError: If there are any invalid indices of the shots axis. + IndexError: If there are any invalid indices of the shots axis. """ if isinstance(indices, int): indices = (indices,) for index in indices: if index < 0 or index >= self.num_shots: - raise ValueError( + raise IndexError( f"index {index} is out of bounds for the number of shots {self.num_shots}." ) arr = self._array diff --git a/releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml b/releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml new file mode 100644 index 000000000000..56fd81012428 --- /dev/null +++ b/releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml @@ -0,0 +1,11 @@ +--- +upgrade_primitives: + - | + :meth:`.BitArray.slice_bits` and :meth:`.BitArray.slice_shots` + will now raise ``IndexError`` when indices are out of bounds. + They used to raise ``ValueError`` in the case. + - | + :meth:`.BitArray.__getitem__` will now raise ``IndexError`` + when indices are out of bounds or the number of dimensions + of indices does not match that of BitArray. + They used to raise ``ValueError`` in the case. diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index bd41d127689d..fb99ab0332e4 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -537,6 +537,20 @@ def test_getitem(self): for j in range(2): self.assertEqual(ba.get_counts((0, j, 2)), ba2.get_counts(j)) + with self.subTest("errors"): + with self.assertRaisesRegex(IndexError, "index 2 is out of bounds"): + _ = ba[0, 2, 2] + with self.assertRaisesRegex(IndexError, "index -3 is out of bounds"): + _ = ba[0, -3, 2] + with self.assertRaisesRegex( + IndexError, "BitArray cannot be sliced along the shots axis" + ): + _ = ba[0, 1, 2, 3] + with self.assertRaisesRegex( + IndexError, "BitArray cannot be sliced along the bits axis" + ): + _ = ba[0, 1, 2, 3, 4] + def test_slice_bits(self): """Test the slice_bits method.""" # this creates incrementing bitstrings from 0 to 59 @@ -581,9 +595,9 @@ def test_slice_bits(self): self.assertEqual(ba2.get_counts((i, j, k)), expect) with self.subTest("errors"): - with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + with self.assertRaisesRegex(IndexError, "index -1 is out of bounds"): _ = ba.slice_bits(-1) - with self.assertRaisesRegex(ValueError, "index 9 is out of bounds"): + with self.assertRaisesRegex(IndexError, "index 9 is out of bounds"): _ = ba.slice_bits(9) def test_slice_shots(self): @@ -631,9 +645,9 @@ def test_slice_shots(self): self.assertEqual(ba2.get_bitstrings((i, j, k)), expected) with self.subTest("errors"): - with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + with self.assertRaisesRegex(IndexError, "index -1 is out of bounds"): _ = ba.slice_shots(-1) - with self.assertRaisesRegex(ValueError, "index 10 is out of bounds"): + with self.assertRaisesRegex(IndexError, "index 10 is out of bounds"): _ = ba.slice_shots(10) def test_expectation_values(self): From b23c545233f5c3fad24250ac3112587c6e9f743b Mon Sep 17 00:00:00 2001 From: Henry Zou <87874865+henryzou50@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:14:25 -0400 Subject: [PATCH 12/27] Port `star_preroute` to rust (#12761) * This commit ports the core logic of `star_preroute` from Python to Rust. The changes involve creating a new Rust module for the star prerouting algorithm and updating the corresponding Python code to integrate with this new Rust functionality. Details: - New Rust file: Added `star_preroute.rs` to handle the core logic of the function `star_preroute` from the python side. This file defines the type aliases for node and block representations, which matches the current block representation of the `StarBlock` (except that the center is just a bool, as we only need to know if there is a center), and the node representation matches how the nodes used in `SabreDAG`. The `star_preroute` function processes the star blocks witihin the `SabreDAG` and finds the linear routing equivalent and then returns the result as a `SabreResult`. Thus we can use the same methods we used in Sabre, such as `_build_sabre_dag` and `_apply_sabre_result`. - Node representation: A key part of this implementation is how it takes advantage of `SabreResult` and `SabreDAG`, so the node representation is a tuple of the node id, list of qubit indices, a set of classical bit indices, and a directive flag. However, once we update the regular DAG to rust, this part may change significantly. - Updates in the SABRE rust module: To use `sabre_dag.rs` and `swap_map.rs` in `star_prerouting`, I change them to be public in `crates/accelerate/src/sabre/mod.rs`. Not sure if it makes more sense to do it this way or to move `star_prerouting` to `crates/accelerate/src/sabre/` since it mimics the methods used in Sabre to change the dag. - Python side updates: Imported the necessary modules and only modified the function `star_preroute` so that now the function performs the heavy lifting of transforming the DAG within the Rust space, leveraging `_build_sabre_dag` and `_apply_sabre_result`. - Possible issues: I was not sure how correctly handle control flow from the rust side. I know that `route.rs` handles this with `route_control_flow_block` to populate the `node_block_results` for `SabreResult`, but I was not sure how to best take advantage of this function for `star_prerouting`. Currently, the `node_block_results` for `star_prerouting` essentially always empty and just there to have`SabreResult`. There also seems to be no unit tests for `star_prerouting` that includes control flow. * lint * Added additional test and adjust the swap map result - Added the additional test of qft linearization and that the resultings circuit has `n-2` swap gates where `n` is the number of cp gates. - Changed the `node_id` in `apply_swap` of `star_preroute.rs` to use the current node id as it is more efficient, but just does not match how we do it in Sabre. This makes it so that we apply the gate first then the swap, which fixes an error we had before where we never placed a swap gate at the end of the processing a block. This only affected tests where we had multiple blocks to process. To make sure we apply the results correctly from `SabreResult`, I added a flag to `_apply_sabre_result` to treat the case of `StarPrerouting` differently so that it applies the swap after applying the node. - Added a hasp map from node to block to make processing each node in the given processing order have `n + n` time complexity instead of `n^2`. As a result, we can also remove the function `find_block_id` * Reverted changes to `_apply_sabre_result` and fixed handling on rust side - Removed `apply_swap_first` flag in `_apply_sabre_result` as it did not make sense to have it as there are no other scenario where a user may want to have control over applying the swap first. - To adjust for this and make `star_preroute` consistent with `apply_sabre_result` to apply swaps before the node id on the swap map, I adjusted `star_preroute.rs` to first process the blocks to gather the swap locations and the gate order. Once we have the full gate order, we can use the swap locations to apply the swaps while knowing the `qargs` of the node before the swap and the `node_id` of the node after the swap. - Since the above in done in the main `star_preroute` function, I removed `qubit_ampping` and `out_map` as arguments for `process_blocks`. --- crates/accelerate/src/lib.rs | 1 + crates/accelerate/src/sabre/mod.rs | 4 +- crates/accelerate/src/star_prerouting.rs | 214 ++++++++++++++++++ crates/pyext/src/lib.rs | 8 +- qiskit/__init__.py | 1 + .../passes/routing/star_prerouting.py | 181 +++++++-------- ...port_star_prerouting-13fae3ff78feb5e3.yaml | 11 + .../python/transpiler/test_star_prerouting.py | 21 ++ 8 files changed, 333 insertions(+), 108 deletions(-) create mode 100644 crates/accelerate/src/star_prerouting.rs create mode 100644 releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 314fa5ff7c58..4e079ea84b57 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -27,6 +27,7 @@ pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; +pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; pub mod target_transpiler; diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 3eb8ebb3a219..1229be16b723 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -14,8 +14,8 @@ mod layer; mod layout; mod neighbor_table; mod route; -mod sabre_dag; -mod swap_map; +pub mod sabre_dag; +pub mod swap_map; use hashbrown::HashMap; use numpy::{IntoPyArray, ToPyArray}; diff --git a/crates/accelerate/src/star_prerouting.rs b/crates/accelerate/src/star_prerouting.rs new file mode 100644 index 000000000000..fd2156ad2011 --- /dev/null +++ b/crates/accelerate/src/star_prerouting.rs @@ -0,0 +1,214 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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. + +/// Type alias for a node representation. +/// Each node is represented as a tuple containing: +/// - Node id (usize) +/// - List of involved qubit indices (Vec) +/// - Set of involved classical bit indices (HashSet) +/// - Directive flag (bool) +type Nodes = (usize, Vec, HashSet, bool); + +/// Type alias for a block representation. +/// Each block is represented by a tuple containing: +/// - A boolean indicating the presence of a center (bool) +/// - A list of nodes (Vec) +type Block = (bool, Vec); + +use crate::nlayout::PhysicalQubit; +use crate::nlayout::VirtualQubit; +use crate::sabre::sabre_dag::SabreDAG; +use crate::sabre::swap_map::SwapMap; +use crate::sabre::BlockResult; +use crate::sabre::NodeBlockResults; +use crate::sabre::SabreResult; +use hashbrown::HashMap; +use hashbrown::HashSet; +use numpy::IntoPyArray; +use pyo3::prelude::*; + +/// Python function to perform star prerouting on a SabreDAG. +/// This function processes star blocks and updates the DAG and qubit mapping. +#[pyfunction] +#[pyo3(text_signature = "(dag, blocks, processing_order, /)")] +fn star_preroute( + py: Python, + dag: &mut SabreDAG, + blocks: Vec, + processing_order: Vec, +) -> (SwapMap, PyObject, NodeBlockResults, PyObject) { + let mut qubit_mapping: Vec = (0..dag.num_qubits).collect(); + let mut processed_block_ids: HashSet = HashSet::with_capacity(blocks.len()); + let last_2q_gate = processing_order.iter().rev().find(|node| node.1.len() == 2); + let mut is_first_star = true; + + // Structures for SabreResult + let mut out_map: HashMap> = + HashMap::with_capacity(dag.dag.node_count()); + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + let node_block_results: HashMap> = HashMap::new(); + + // Create a HashMap to store the node-to-block mapping + let mut node_to_block: HashMap = HashMap::with_capacity(processing_order.len()); + for (block_id, block) in blocks.iter().enumerate() { + for node in &block.1 { + node_to_block.insert(node.0, block_id); + } + } + // Store nodes where swaps will be placed. + let mut swap_locations: Vec<&Nodes> = Vec::with_capacity(processing_order.len()); + + // Process blocks, gathering swap locations and updating the gate order + for node in &processing_order { + if let Some(&block_id) = node_to_block.get(&node.0) { + // Skip if the block has already been processed + if !processed_block_ids.insert(block_id) { + continue; + } + process_block( + &blocks[block_id], + last_2q_gate, + &mut is_first_star, + &mut gate_order, + &mut swap_locations, + ); + } else { + // Apply operation for nodes not part of any block + gate_order.push(node.0); + } + } + + // Apply the swaps based on the gathered swap locations and gate order + for (index, node_id) in gate_order.iter().enumerate() { + for swap_location in &swap_locations { + if *node_id == swap_location.0 { + if let Some(next_node_id) = gate_order.get(index + 1) { + apply_swap( + &mut qubit_mapping, + &swap_location.1, + *next_node_id, + &mut out_map, + ); + } + } + } + } + + let res = SabreResult { + map: SwapMap { map: out_map }, + node_order: gate_order, + node_block_results: NodeBlockResults { + results: node_block_results, + }, + }; + + let final_res = ( + res.map, + res.node_order.into_pyarray_bound(py).into(), + res.node_block_results, + qubit_mapping.into_pyarray_bound(py).into(), + ); + + final_res +} + +/// Processes a star block, applying operations and handling swaps. +/// +/// Args: +/// +/// * `block` - A tuple containing a boolean indicating the presence of a center and a vector of nodes representing the star block. +/// * `last_2q_gate` - The last two-qubit gate in the processing order. +/// * `is_first_star` - A mutable reference to a boolean indicating if this is the first star block being processed. +/// * `gate_order` - A mutable reference to the gate order vector. +/// * `swap_locations` - A mutable reference to the nodes where swaps will be placed after +fn process_block<'a>( + block: &'a Block, + last_2q_gate: Option<&'a Nodes>, + is_first_star: &mut bool, + gate_order: &mut Vec, + swap_locations: &mut Vec<&'a Nodes>, +) { + let (has_center, sequence) = block; + + // If the block contains exactly 2 nodes, apply them directly + if sequence.len() == 2 { + for inner_node in sequence { + gate_order.push(inner_node.0); + } + return; + } + + let mut prev_qargs = None; + let mut swap_source = false; + + // Process each node in the block + for inner_node in sequence.iter() { + // Apply operation directly if it's a single-qubit operation or the same as previous qargs + if inner_node.1.len() == 1 || prev_qargs == Some(&inner_node.1) { + gate_order.push(inner_node.0); + continue; + } + + // If this is the first star and no swap source has been identified, set swap_source + if *is_first_star && !swap_source { + swap_source = *has_center; + gate_order.push(inner_node.0); + prev_qargs = Some(&inner_node.1); + continue; + } + + // Place 2q-gate and subsequent swap gate + gate_order.push(inner_node.0); + + if inner_node != last_2q_gate.unwrap() && inner_node.1.len() == 2 { + swap_locations.push(inner_node); + } + prev_qargs = Some(&inner_node.1); + } + *is_first_star = false; +} + +/// Applies a swap operation to the DAG and updates the qubit mapping. +/// +/// # Args: +/// +/// * `qubit_mapping` - A mutable reference to the qubit mapping vector. +/// * `qargs` - Qubit indices for the swap operation (node before the swap) +/// * `next_node_id` - ID of the next node in the gate order (node after the swap) +/// * `out_map` - A mutable reference to the output map. +fn apply_swap( + qubit_mapping: &mut [usize], + qargs: &[VirtualQubit], + next_node_id: usize, + out_map: &mut HashMap>, +) { + if qargs.len() == 2 { + let idx0 = qargs[0].index(); + let idx1 = qargs[1].index(); + + // Update the `qubit_mapping` and `out_map` to reflect the swap operation + qubit_mapping.swap(idx0, idx1); + out_map.insert( + next_node_id, + vec![[ + PhysicalQubit::new(qubit_mapping[idx0].try_into().unwrap()), + PhysicalQubit::new(qubit_mapping[idx1].try_into().unwrap()), + ]], + ); + } +} + +#[pymodule] +pub fn star_prerouting(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(star_preroute))?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 6af99ff04a8d..f9711641a938 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -18,9 +18,10 @@ use qiskit_accelerate::{ error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, - sparse_pauli_op::sparse_pauli_op, stochastic_swap::stochastic_swap, synthesis::synthesis, - target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, - utils::utils, vf2_layout::vf2_layout, + sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, + stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, + vf2_layout::vf2_layout, }; #[pymodule] @@ -41,6 +42,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op))?; + m.add_wrapped(wrap_pymodule!(star_prerouting))?; m.add_wrapped(wrap_pymodule!(stochastic_swap))?; m.add_wrapped(wrap_pymodule!(target))?; m.add_wrapped(wrap_pymodule!(two_qubit_decompose))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index d88261cad209..6091bfa90346 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -77,6 +77,7 @@ sys.modules["qiskit._accelerate.sabre"] = _accelerate.sabre sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op +sys.modules["qiskit._accelerate.star_prerouting"] = _accelerate.star_prerouting sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.target"] = _accelerate.target sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index c00cee74de4b..53bc971a268b 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -14,11 +14,15 @@ from typing import Iterable, Union, Optional, List, Tuple from math import floor, log10 -from qiskit.circuit import Barrier -from qiskit.circuit.library import SwapGate +from qiskit.circuit import SwitchCaseOp, Clbit, ClassicalRegister, Barrier +from qiskit.circuit.controlflow import condition_resources, node_resources from qiskit.dagcircuit import DAGOpNode, DAGDepNode, DAGDependency, DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.layout import Layout +from qiskit.transpiler.passes.routing.sabre_swap import _build_sabre_dag, _apply_sabre_result + +from qiskit._accelerate import star_prerouting +from qiskit._accelerate.nlayout import NLayout class StarBlock: @@ -305,113 +309,84 @@ def star_preroute(self, dag, blocks, processing_order): new_dag: a dag specifying the pre-routed circuit qubit_mapping: the final qubit mapping after pre-routing """ - node_to_block_id = {} - for i, block in enumerate(blocks): - for node in block.get_nodes(): - node_to_block_id[node] = i - - new_dag = dag.copy_empty_like() - processed_block_ids = set() - qubit_mapping = list(range(len(dag.qubits))) - - def _apply_mapping(qargs, qubit_mapping, qubits): - return tuple(qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) - - is_first_star = True - last_2q_gate = [ - op - for op in reversed(processing_order) - if ((len(op.qargs) > 1) and (op.name != "barrier")) + # Convert the DAG to a SabreDAG + num_qubits = len(dag.qubits) + canonical_register = dag.qregs["q"] + current_layout = Layout.generate_trivial_layout(canonical_register) + qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + layout_mapping = {qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items()} + initial_layout = NLayout(layout_mapping, num_qubits, num_qubits) + sabre_dag, circuit_to_dag_dict = _build_sabre_dag(dag, num_qubits, qubit_indices) + + # Extract the nodes from the blocks for the Rust representation + rust_blocks = [ + (block.center is not None, _extract_nodes(block.get_nodes(), dag)) for block in blocks ] - if len(last_2q_gate) > 0: - last_2q_gate = last_2q_gate[0] - else: - last_2q_gate = None + # Determine the processing order of the nodes in the DAG for the Rust representation int_digits = floor(log10(len(processing_order))) + 1 processing_order_index_map = { - node: f"a{str(index).zfill(int(int_digits))}" - for index, node in enumerate(processing_order) + node: f"a{index:0{int_digits}}" for index, node in enumerate(processing_order) } def tie_breaker_key(node): return processing_order_index_map.get(node, node.sort_key) - for node in dag.topological_op_nodes(key=tie_breaker_key): - block_id = node_to_block_id.get(node, None) - if block_id is not None: - if block_id in processed_block_ids: - continue - - processed_block_ids.add(block_id) - - # process the whole block - block = blocks[block_id] - sequence = block.nodes - center_node = block.center - - if len(sequence) == 2: - for inner_node in sequence: - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - continue - swap_source = None - prev = None - for inner_node in sequence: - if (len(inner_node.qargs) == 1) or (inner_node.qargs == prev): - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - continue - if is_first_star and swap_source is None: - swap_source = center_node - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - - prev = inner_node.qargs - continue - # place 2q-gate and subsequent swap gate - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - - if not inner_node is last_2q_gate and not isinstance(inner_node.op, Barrier): - new_dag.apply_operation_back( - SwapGate(), - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - # Swap mapping - index_0 = dag.find_bit(inner_node.qargs[0]).index - index_1 = dag.find_bit(inner_node.qargs[1]).index - qubit_mapping[index_1], qubit_mapping[index_0] = ( - qubit_mapping[index_0], - qubit_mapping[index_1], - ) - - prev = inner_node.qargs - is_first_star = False - else: - # the node is not part of a block - new_dag.apply_operation_back( - node.op, - _apply_mapping(node.qargs, qubit_mapping, dag.qubits), - node.cargs, - check=False, - ) - return new_dag, qubit_mapping + rust_processing_order = _extract_nodes(dag.topological_op_nodes(key=tie_breaker_key), dag) + + # Run the star prerouting algorithm to obtain the new DAG and qubit mapping + *sabre_result, qubit_mapping = star_prerouting.star_preroute( + sabre_dag, rust_blocks, rust_processing_order + ) + + res_dag = _apply_sabre_result( + dag.copy_empty_like(), + dag, + sabre_result, + initial_layout, + dag.qubits, + circuit_to_dag_dict, + ) + + return res_dag, qubit_mapping + + +def _extract_nodes(nodes, dag): + """Extract and format node information for Rust representation used in SabreDAG. + + Each node is represented as a tuple containing: + - Node ID (int): The unique identifier of the node in the DAG. + - Qubit indices (list of int): Indices of qubits involved in the node's operation. + - Classical bit indices (set of int): Indices of classical bits involved in the node's operation. + - Directive flag (bool): Indicates whether the operation is a directive (True) or not (False). + + Args: + nodes (list[DAGOpNode]): List of DAGOpNode objects to extract information from. + dag (DAGCircuit): DAGCircuit object containing the circuit structure. + + Returns: + list of tuples: Each tuple contains information about a node in the format described above. + """ + extracted_node_info = [] + for node in nodes: + qubit_indices = [dag.find_bit(qubit).index for qubit in node.qargs] + classical_bit_indices = set() + + if node.op.condition is not None: + classical_bit_indices.update(condition_resources(node.op.condition).clbits) + + if isinstance(node.op, SwitchCaseOp): + switch_case_target = node.op.target + if isinstance(switch_case_target, Clbit): + classical_bit_indices.add(switch_case_target) + elif isinstance(switch_case_target, ClassicalRegister): + classical_bit_indices.update(switch_case_target) + else: # Assume target is an expression involving classical bits + classical_bit_indices.update(node_resources(switch_case_target).clbits) + + is_directive = getattr(node.op, "_directive", False) + extracted_node_info.append( + (node._node_id, qubit_indices, classical_bit_indices, is_directive) + ) + + return extracted_node_info diff --git a/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml b/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml new file mode 100644 index 000000000000..f8eca807bec6 --- /dev/null +++ b/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml @@ -0,0 +1,11 @@ +--- +features_transpiler: + - | + Port part of the logic from the :class:`StarPrerouting`, used to + find a star graph connectivity subcircuit and replaces it with a + linear routing equivalent. + - | + The function :func:`star_preroute` now performs the heavily lifting + to transform the dag by in the rust space by taking advantage + of the functions :func:`_build_sabre_dag` and + :func:`_apply_sabre_result`. diff --git a/test/python/transpiler/test_star_prerouting.py b/test/python/transpiler/test_star_prerouting.py index fb67698300b4..2744113f13cb 100644 --- a/test/python/transpiler/test_star_prerouting.py +++ b/test/python/transpiler/test_star_prerouting.py @@ -24,6 +24,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.quantum_info import Operator from qiskit.transpiler.passes import VF2Layout, ApplyLayout, SabreSwap, SabreLayout +from qiskit.transpiler.passes.layout.vf2_utils import build_interaction_graph from qiskit.transpiler.passes.routing.star_prerouting import StarPreRouting from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.passmanager import PassManager @@ -480,3 +481,23 @@ def test_routing_after_star_prerouting(self): self.assertTrue(Operator.from_circuit(res_sabre), qc) self.assertTrue(Operator.from_circuit(res_star), qc) self.assertTrue(Operator.from_circuit(res_star), Operator.from_circuit(res_sabre)) + + @ddt.data(4, 8, 16, 32) + def test_qft_linearization(self, num_qubits): + """Test the QFT circuit to verify if it is linearized and requires n-2 swaps.""" + + qc = QFT(num_qubits, do_swaps=False, insert_barriers=True).decompose() + dag = circuit_to_dag(qc) + new_dag = StarPreRouting().run(dag) + new_qc = dag_to_circuit(new_dag) + + # Check that resulting result has n-2 swaps, where n is the number of cp gates + swap_count = new_qc.count_ops().get("swap", 0) + cp_count = new_qc.count_ops().get("cp", 0) + self.assertEqual(swap_count, cp_count - 2) + + # Confirm linearization by checking that the number of edges is equal to the number of nodes + interaction_graph = build_interaction_graph(new_dag, strict_direction=False)[0] + num_edges = interaction_graph.num_edges() + num_nodes = interaction_graph.num_nodes() + self.assertEqual(num_edges, num_nodes - 1) From b985b96b9d3b94dffe60fc4980872dd1ecf6f98e Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 29 Jul 2024 19:29:53 +0200 Subject: [PATCH 13/27] Change the default `routing_method` to `sabre` (#12806) * change the default routing_method to sabre * reno * Update releasenotes/notes/sabre_level0-1524f01965257f3f.yaml Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/transpiler/preset_passmanagers/level0.py | 2 +- .../notes/sabre_level0-1524f01965257f3f.yaml | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/sabre_level0-1524f01965257f3f.yaml diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 7289a8635c55..58381b3db7a8 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -48,7 +48,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa initial_layout = pass_manager_config.initial_layout init_method = pass_manager_config.init_method or "default" layout_method = pass_manager_config.layout_method or "default" - routing_method = pass_manager_config.routing_method or "stochastic" + routing_method = pass_manager_config.routing_method or "sabre" translation_method = pass_manager_config.translation_method or "translator" optimization_method = pass_manager_config.optimization_method or "default" scheduling_method = pass_manager_config.scheduling_method or "default" diff --git a/releasenotes/notes/sabre_level0-1524f01965257f3f.yaml b/releasenotes/notes/sabre_level0-1524f01965257f3f.yaml new file mode 100644 index 000000000000..c24775d6d096 --- /dev/null +++ b/releasenotes/notes/sabre_level0-1524f01965257f3f.yaml @@ -0,0 +1,13 @@ +--- +upgrade_transpiler: + - | + The default routing pass used by optimization level 0 for :func:`.generate_preset_pass_manager` + and :func:`.transpile` has been changed from :class:`.StochasticSwap` to :class:`.SabreSwap`. + The :class:`.SabreSwap` pass performs exactly the same function but performs better in both + runtime and output quality (in number of swap gates and depth) compared to + :class:`.StochasticSwap`. For ``optimization_level=0`` this shouldn't matter because it's not + expected to run routing for the typical use case of level 0. + + If you were relying on the previous default routing algorithm for any reason you can use the + ``routing_method`` argument for :func:`.transpile` and :func:`.generate_preset_pass_manager` + to ``"stochastic"`` to use the :class:`.StochasticSwap` pass. From 239a669ab72527f2b6aec62eb8f1886dd493f14f Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:04:51 +0900 Subject: [PATCH 14/27] Fix `StatevectorSampler` to raise an error if a circuit with c_if is passed (#12842) * fix StatevectorSampler with c_if * add comment --- qiskit/primitives/statevector_sampler.py | 7 +++-- ...evector-sampler-c_if-9753f8f97a3d0ff5.yaml | 5 ++++ .../primitives/test_statevector_sampler.py | 28 +++++++++++++------ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index 7488faa284de..58980cb79f6e 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -207,7 +207,7 @@ def _preprocess_circuit(circuit: QuantumCircuit): qargs_index = {v: k for k, v in enumerate(qargs)} circuit = circuit.remove_final_measurements(inplace=False) if _has_control_flow(circuit): - raise QiskitError("StatevectorSampler cannot handle ControlFlowOp") + raise QiskitError("StatevectorSampler cannot handle ControlFlowOp and c_if") if _has_measure(circuit): raise QiskitError("StatevectorSampler cannot handle mid-circuit measurements") # num_qubits is used as sentinel to fill 0 in _samples_to_packed_array @@ -283,4 +283,7 @@ def _final_measurement_mapping(circuit: QuantumCircuit) -> dict[tuple[ClassicalR def _has_control_flow(circuit: QuantumCircuit) -> bool: - return any(isinstance(instruction.operation, ControlFlowOp) for instruction in circuit) + return any( + isinstance((op := instruction.operation), ControlFlowOp) or op.condition + for instruction in circuit + ) diff --git a/releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml b/releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml new file mode 100644 index 000000000000..d743b52ee900 --- /dev/null +++ b/releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug of :class:`.StatevectorSampler` that ignored gates with ``c_if``. + It will raise an error because :class:`.Statevector` cannot handle ``c_if``. diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index c065871025d7..1f84c072564d 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -280,6 +280,11 @@ def test_run_errors(self): qc3 = QuantumCircuit(1, 1) with qc3.for_loop(range(5)): qc3.h(0) + qc4 = QuantumCircuit(2, 2) + qc4.h(0) + qc4.measure(1, 1) + qc4.x(0).c_if(1, 1) + qc4.measure(0, 0) sampler = StatevectorSampler() with self.subTest("set parameter values to a non-parameterized circuit"): @@ -301,6 +306,9 @@ def test_run_errors(self): with self.subTest("with control flow"): with self.assertRaises(QiskitError): _ = sampler.run([qc3]).result() + with self.subTest("with c_if"): + with self.assertRaises(QiskitError): + _ = sampler.run([qc4]).result() with self.subTest("negative shots, run arg"): with self.assertRaises(ValueError): _ = sampler.run([qc1], shots=-1).result() @@ -584,17 +592,21 @@ def test_circuit_with_aliased_cregs(self): c2 = ClassicalRegister(1, "c2") qc = QuantumCircuit(q, c1, c2) - qc.ry(np.pi / 4, 2) - qc.cx(2, 1) - qc.cx(0, 1) - qc.h(0) - qc.measure(0, c1) - qc.measure(1, c2) qc.z(2).c_if(c1, 1) qc.x(2).c_if(c2, 1) qc2 = QuantumCircuit(5, 5) qc2.compose(qc, [0, 2, 3], [2, 4], inplace=True) - cregs = [creg.name for creg in qc2.cregs] + # Note: qc2 has aliased cregs, c0 -> c[2] and c1 -> c[4]. + # copy_empty_like copies the aliased cregs of qc2 to qc3. + qc3 = QuantumCircuit.copy_empty_like(qc2) + qc3.ry(np.pi / 4, 2) + qc3.cx(2, 1) + qc3.cx(0, 1) + qc3.h(0) + qc3.measure(0, 2) + qc3.measure(1, 4) + self.assertEqual(len(qc3.cregs), 3) + cregs = [creg.name for creg in qc3.cregs] target = { cregs[0]: {0: 4255, 4: 4297, 16: 720, 20: 726}, cregs[1]: {0: 5000, 1: 5000}, @@ -602,7 +614,7 @@ def test_circuit_with_aliased_cregs(self): } sampler = StatevectorSampler(seed=self._seed) - result = sampler.run([qc2], shots=self._shots).result() + result = sampler.run([qc3], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data self.assertEqual(len(data), 3) From 85f98605a6488d9e25e5cd04eedc3687ebece614 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Jul 2024 14:23:26 +0100 Subject: [PATCH 15/27] Fix parsing of huge OpenQASM 2 conditionals (#12774) * Fix parsing of huge OpenQASM 2 conditionals We fixed handling of giant integers in gate expression positions gh-12140, and this commit fixes the handling in conditionals. Unfortunately, this means pulling in big-int handling properly; the integers simply _are_ bigints, and we're not immediately converting them into something else. The need to support this may influence how the Rust-space data models of `QuantumCircuit` evolve. * Move `num-bigint` dependency to workspace --- Cargo.lock | 1 + Cargo.toml | 1 + crates/accelerate/Cargo.toml | 2 +- crates/qasm2/Cargo.toml | 1 + crates/qasm2/src/bytecode.rs | 7 +++-- crates/qasm2/src/lex.rs | 10 +++++++ crates/qasm2/src/parse.rs | 18 +++++++----- .../qasm2-big-condition-cfd203d53540d4ca.yaml | 6 ++++ test/python/qasm2/test_structure.py | 29 +++++++++++++++++++ 9 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml diff --git a/Cargo.lock b/Cargo.lock index e3416426b4d1..f91bab4d5603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,6 +1213,7 @@ name = "qiskit-qasm2" version = "1.3.0" dependencies = [ "hashbrown 0.14.5", + "num-bigint", "pyo3", "qiskit-circuit", ] diff --git a/Cargo.toml b/Cargo.toml index a10792b9ad0f..1e8a7185f449 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ license = "Apache-2.0" bytemuck = "1.16" indexmap.version = "2.2.6" hashbrown.version = "0.14.0" +num-bigint = "0.4" num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 9d6024783996..0b23cf08743e 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -18,7 +18,7 @@ rand_distr = "0.4.3" ahash = "0.8.11" num-traits = "0.2" num-complex.workspace = true -num-bigint = "0.4" +num-bigint.workspace = true rustworkx-core = "0.15" faer = "0.19.1" itertools = "0.13.0" diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 68137ee602bf..681693c4a17d 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -10,6 +10,7 @@ name = "qiskit_qasm2" doctest = false [dependencies] +num-bigint.workspace = true hashbrown.workspace = true pyo3.workspace = true qiskit-circuit.workspace = true diff --git a/crates/qasm2/src/bytecode.rs b/crates/qasm2/src/bytecode.rs index 2dea3a2b0e85..fab973f2186f 100644 --- a/crates/qasm2/src/bytecode.rs +++ b/crates/qasm2/src/bytecode.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use num_bigint::BigUint; use pyo3::prelude::*; use crate::error::QASM2ParseError; @@ -160,7 +161,7 @@ pub enum InternalBytecode { arguments: Vec, qubits: Vec, creg: CregId, - value: usize, + value: BigUint, }, Measure { qubit: QubitId, @@ -170,7 +171,7 @@ pub enum InternalBytecode { qubit: QubitId, clbit: ClbitId, creg: CregId, - value: usize, + value: BigUint, }, Reset { qubit: QubitId, @@ -178,7 +179,7 @@ pub enum InternalBytecode { ConditionedReset { qubit: QubitId, creg: CregId, - value: usize, + value: BigUint, }, Barrier { qubits: Vec, diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index 551fd2b7af48..cfac9e98fce0 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -24,6 +24,7 @@ //! real-number tokenization. use hashbrown::HashMap; +use num_bigint::BigUint; use pyo3::prelude::PyResult; use std::path::Path; @@ -279,6 +280,15 @@ impl Token { context.text[self.index].parse().unwrap() } + /// If the token is an integer (by type, not just by value), this method can be called to + /// evaluate its value as a big integer. Panics if the token is not an integer type. + pub fn bigint(&self, context: &TokenContext) -> BigUint { + if self.ttype != TokenType::Integer { + panic!() + } + context.text[self.index].parse().unwrap() + } + /// If the token is a filename path, this method can be called to get a (regular) string /// representing it. Panics if the token type was not a filename. pub fn filename(&self, context: &TokenContext) -> String { diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index f7eceb6aeef4..36e2a49f669c 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -16,6 +16,7 @@ //! operator-precedence parser. use hashbrown::{HashMap, HashSet}; +use num_bigint::BigUint; use pyo3::prelude::{PyObject, PyResult, Python}; use crate::bytecode::InternalBytecode; @@ -188,9 +189,10 @@ enum GateParameters { /// An equality condition from an `if` statement. These can condition gate applications, measures /// and resets, although in practice they're basically only ever used on gates. +#[derive(Clone)] struct Condition { creg: CregId, - value: usize, + value: BigUint, } /// Find the first match for the partial [filename] in the directories along [path]. Returns @@ -1105,7 +1107,7 @@ impl State { } { return match parameters { GateParameters::Constant(parameters) => { - self.emit_single_global_gate(bc, gate_id, parameters, qubits, &condition) + self.emit_single_global_gate(bc, gate_id, parameters, qubits, condition) } GateParameters::Expression(parameters) => { self.emit_single_gate_gate(bc, gate_id, parameters, qubits) @@ -1174,7 +1176,7 @@ impl State { } return match parameters { GateParameters::Constant(parameters) => { - self.emit_single_global_gate(bc, gate_id, parameters, qubits, &condition) + self.emit_single_global_gate(bc, gate_id, parameters, qubits, condition) } GateParameters::Expression(parameters) => { self.emit_single_gate_gate(bc, gate_id, parameters, qubits) @@ -1196,7 +1198,7 @@ impl State { gate_id, parameters.clone(), qubits, - &condition, + condition.clone(), )?; } // Gates used in gate-body definitions can't ever broadcast, because their only @@ -1215,7 +1217,7 @@ impl State { gate_id: GateId, arguments: Vec, qubits: Vec, - condition: &Option, + condition: Option, ) -> PyResult { if let Some(condition) = condition { bc.push(Some(InternalBytecode::ConditionedGate { @@ -1262,7 +1264,7 @@ impl State { self.expect(TokenType::Equals, "'=='", &if_token)?; let value = self .expect(TokenType::Integer, "an integer", &if_token)? - .int(&self.context); + .bigint(&self.context); self.expect(TokenType::RParen, "')'", &lparen_token)?; let name = name_token.id(&self.context); let creg = match self.symbols.get(&name) { @@ -1408,7 +1410,7 @@ impl State { qubit: q_start + i, clbit: c_start + i, creg, - value, + value: value.clone(), }) })); Ok(q_size) @@ -1477,7 +1479,7 @@ impl State { Some(InternalBytecode::ConditionedReset { qubit: start + offset, creg, - value, + value: value.clone(), }) })); Ok(size) diff --git a/releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml b/releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml new file mode 100644 index 000000000000..a5863cae2310 --- /dev/null +++ b/releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The OpenQASM 2 parser (:mod:`qiskit.qasm2`) can now handle conditionals + with integers that do not fit within a 64-bit integer. Fixed + `#12773 `__. diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index 22eff30b38f4..d963eea7e255 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -324,6 +324,35 @@ def test_parameterless_gates_accept_parentheses(self): qc.cx(1, 0) self.assertEqual(parsed, qc) + def test_huge_conditions(self): + # Something way bigger than any native integer. + bigint = (1 << 300) + 123456789 + program = f""" + qreg qr[2]; + creg cr[2]; + creg cond[500]; + if (cond=={bigint}) U(0, 0, 0) qr[0]; + if (cond=={bigint}) U(0, 0, 0) qr; + if (cond=={bigint}) reset qr[0]; + if (cond=={bigint}) reset qr; + if (cond=={bigint}) measure qr[0] -> cr[0]; + if (cond=={bigint}) measure qr -> cr; + """ + parsed = qiskit.qasm2.loads(program) + qr, cr = QuantumRegister(2, "qr"), ClassicalRegister(2, "cr") + cond = ClassicalRegister(500, "cond") + qc = QuantumCircuit(qr, cr, cond) + qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) + qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) + qc.u(0, 0, 0, qr[1]).c_if(cond, bigint) + qc.reset(qr[0]).c_if(cond, bigint) + qc.reset(qr[0]).c_if(cond, bigint) + qc.reset(qr[1]).c_if(cond, bigint) + qc.measure(qr[0], cr[0]).c_if(cond, bigint) + qc.measure(qr[0], cr[0]).c_if(cond, bigint) + qc.measure(qr[1], cr[1]).c_if(cond, bigint) + self.assertEqual(parsed, qc) + class TestGateDefinition(QiskitTestCase): def test_simple_definition(self): From 37b334fb735b6044db98e5a197e789fb0d533b22 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Jul 2024 09:50:47 -0400 Subject: [PATCH 16/27] Recreate full dag instead of inplace substitution in BasisTranslator (#12195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Recreate full dag instead of inplace substitution in BasisTranslator This commit tweaks the internal logic of the basis translator transpiler pass to do a full dag recreation instead of inplace modification. If only a few operations were to be substituted it would probably be more efficient to do an inplace modification, but in general the basis translator ends up replacing far more operations than not. In such cases just iterating over the dag and rebuilding it is more efficient because the overhead of `apply_operation_back()` is minimal compared to `substitute_node_with_dag()` (although it's higher than `subtitute_node(.., inplace=True)`). * Return boolean together with dag in 'apply_translation' to maintain original 'flow_blocks' logic and fix drawer test. * Remove print --------- Co-authored-by: Elena Peña Tapia --- .../passes/basis/basis_translator.py | 81 +++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 30b25b271755..8bf98f38e087 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -249,8 +249,9 @@ def run(self, dag): replace_start_time = time.time() def apply_translation(dag, wire_map): - dag_updated = False - for node in dag.op_nodes(): + is_updated = False + out_dag = dag.copy_empty_like() + for node in dag.topological_op_nodes(): node_qargs = tuple(wire_map[bit] for bit in node.qargs) qubit_set = frozenset(node_qargs) if node.name in target_basis or len(node.qargs) < self._min_qubits: @@ -258,45 +259,48 @@ def apply_translation(dag, wire_map): flow_blocks = [] for block in node.op.blocks: dag_block = circuit_to_dag(block) - dag_updated = apply_translation( + updated_dag, is_updated = apply_translation( dag_block, { inner: wire_map[outer] for inner, outer in zip(block.qubits, node.qargs) }, ) - if dag_updated: - flow_circ_block = dag_to_circuit(dag_block) + if is_updated: + flow_circ_block = dag_to_circuit(updated_dag) else: flow_circ_block = block flow_blocks.append(flow_circ_block) node.op = node.op.replace_blocks(flow_blocks) + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) continue if ( node_qargs in self._qargs_with_non_global_operation and node.name in self._qargs_with_non_global_operation[node_qargs] ): + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) continue if dag.has_calibration_for(node): + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) continue if qubit_set in extra_instr_map: - self._replace_node(dag, node, extra_instr_map[qubit_set]) + self._replace_node(out_dag, node, extra_instr_map[qubit_set]) elif (node.name, node.num_qubits) in instr_map: - self._replace_node(dag, node, instr_map) + self._replace_node(out_dag, node, instr_map) else: raise TranspilerError(f"BasisTranslator did not map {node.name}.") - dag_updated = True - return dag_updated + is_updated = True + return out_dag, is_updated - apply_translation(dag, qarg_indices) + out_dag, _ = apply_translation(dag, qarg_indices) replace_end_time = time.time() logger.info( "Basis translation instructions replaced in %.3fs.", replace_end_time - replace_start_time, ) - return dag + return out_dag def _replace_node(self, dag, node, instr_map): target_params, target_dag = instr_map[node.name, node.num_qubits] @@ -307,12 +311,18 @@ def _replace_node(self, dag, node, instr_map): ) if node.params: parameter_map = dict(zip(target_params, node.params)) - bound_target_dag = target_dag.copy_empty_like() for inner_node in target_dag.topological_op_nodes(): new_node = DAGOpNode.from_instruction( inner_node._to_circuit_instruction(), - dag=bound_target_dag, + dag=target_dag, ) + new_node.qargs = tuple( + node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs + ) + new_node.cargs = tuple( + node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs + ) + if not new_node.is_standard_gate: new_node.op = new_node.op.copy() if any(isinstance(x, ParameterExpression) for x in inner_node.params): @@ -334,7 +344,8 @@ def _replace_node(self, dag, node, instr_map): new_node.params = new_params if not new_node.is_standard_gate: new_node.op.params = new_params - bound_target_dag._apply_op_node_back(new_node) + dag._apply_op_node_back(new_node) + if isinstance(target_dag.global_phase, ParameterExpression): old_phase = target_dag.global_phase bind_dict = {x: parameter_map[x] for x in old_phase.parameters} @@ -342,31 +353,37 @@ def _replace_node(self, dag, node, instr_map): new_phase = old_phase for x in bind_dict.items(): new_phase = new_phase.assign(*x) - else: new_phase = old_phase.bind(bind_dict) if not new_phase.parameters: new_phase = new_phase.numeric() if isinstance(new_phase, complex): raise TranspilerError(f"Global phase must be real, but got '{new_phase}'") - bound_target_dag.global_phase = new_phase - else: - bound_target_dag = target_dag - - if len(bound_target_dag.op_nodes()) == 1 and len( - bound_target_dag.op_nodes()[0].qargs - ) == len(node.qargs): - dag_op = bound_target_dag.op_nodes()[0].op - # dag_op may be the same instance as other ops in the dag, - # so if there is a condition, need to copy - if getattr(node, "condition", None): - dag_op = dag_op.copy() - dag.substitute_node(node, dag_op, inplace=True) - - if bound_target_dag.global_phase: - dag.global_phase += bound_target_dag.global_phase + dag.global_phase += new_phase + else: - dag.substitute_node_with_dag(node, bound_target_dag) + for inner_node in target_dag.topological_op_nodes(): + new_node = DAGOpNode.from_instruction( + inner_node._to_circuit_instruction(), + dag=target_dag, + ) + new_node.qargs = tuple( + node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs + ) + new_node.cargs = tuple( + node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs + ) + if not new_node.is_standard_gate: + new_node.op = new_node.op.copy() + # dag_op may be the same instance as other ops in the dag, + # so if there is a condition, need to copy + if getattr(node.op, "condition", None): + new_node_op = new_node.op.to_mutable() + new_node_op.condition = node.op.condition + new_node.op = new_node_op + dag._apply_op_node_back(new_node) + if target_dag.global_phase: + dag.global_phase += target_dag.global_phase @singledispatchmethod def _extract_basis(self, circuit): From 43d8372ef7350a348897afa9c7dbd51c4ae3dbe5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Jul 2024 15:08:06 +0100 Subject: [PATCH 17/27] Expose Sabre heuristic configuration to Python (#12171) * Expose Sabre heuristic configuration to Python This exposes the entirety of the configuration of the Sabre heuristic to Python space, making it modifiable without recompilation. This includes some additional configuration options that were not previously easily modifiable, even with recompilation: - the base weight of the "basic" component can be adjusted - the weight of the "basic" and "lookahead" components can be adjusted to _either_ use a constant weight (previously not a thing) or use a weight that scales with the size of the set (previously the only option). - the "decay" component is now entirely separated from the "lookahead" component, so in theory you can now have a decay without a lookahead. This introduces a tracking `Vec` that stores the scores of _all_ the swaps encountered, rather than just dynamically keeping hold of the best swaps. This has a couple of benefits: - with the new dynamic structure for heuristics, this is rather more efficient because each heuristic component can be calculated in separate loops over the swaps, and we don't have to branch within the innermost loop. - it makes it possible in the future to try things like assigning probabilities to each swap and randomly choosing from _all_ of them, not just the best swaps. This is something I've actively wanted to try for quite some time. The default heuristics in the transpiler-pass creators for the `basic`, `lookahead` and `decay` strings are set to represent the same heuristics as before, and this commit is entirely RNG compatible with its predecessor (_technically_ for huge problems there's a possiblity that pulling out some divisions into multiplications by reciprocals will affect the floating-point maths enough to modify the swap selection). * Update for PyO3 0.21 * Increase documentation of heuristic components --- crates/accelerate/src/sabre/heuristic.rs | 284 ++++++++++++++++++ crates/accelerate/src/sabre/layer.rs | 28 +- crates/accelerate/src/sabre/layout.rs | 7 +- crates/accelerate/src/sabre/mod.rs | 15 +- crates/accelerate/src/sabre/route.rs | 181 ++++++----- .../transpiler/passes/layout/sabre_layout.py | 14 +- .../transpiler/passes/routing/sabre_swap.py | 28 +- 7 files changed, 430 insertions(+), 127 deletions(-) create mode 100644 crates/accelerate/src/sabre/heuristic.rs diff --git a/crates/accelerate/src/sabre/heuristic.rs b/crates/accelerate/src/sabre/heuristic.rs new file mode 100644 index 000000000000..32da6e414025 --- /dev/null +++ b/crates/accelerate/src/sabre/heuristic.rs @@ -0,0 +1,284 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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. + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyString; +use pyo3::Python; + +/// Affect the dynamic scaling of the weight of node-set-based heuristics (basic and lookahead). +#[pyclass] +#[pyo3(module = "qiskit._accelerate.sabre", frozen)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SetScaling { + /// No dynamic scaling of the weight. + Constant, + /// Scale the weight by the current number of nodes in the set (e.g., if it contains 5 nodes, + /// the weight will be multiplied by ``0.2``). + Size, +} +#[pymethods] +impl SetScaling { + pub fn __reduce__(&self, py: Python) -> PyResult> { + let name = match self { + SetScaling::Constant => "Constant", + SetScaling::Size => "Size", + }; + Ok(( + py.import_bound("builtins")?.getattr("getattr")?, + (py.get_type_bound::(), name), + ) + .into_py(py)) + } +} + +/// Define the characteristics of the basic heuristic. This is a simple sum of the physical +/// distances of every gate in the front layer. +#[pyclass] +#[pyo3(module = "qiskit._accelerate.sabre", frozen)] +#[derive(Clone, Copy, PartialEq)] +pub struct BasicHeuristic { + /// The relative weighting of this heuristic to others. Typically you should just set this to + /// 1.0 and define everything else in terms of this. + pub weight: f64, + /// Set the dynamic scaling of the weight based on the layer it is applying to. + pub scale: SetScaling, +} +#[pymethods] +impl BasicHeuristic { + #[new] + pub fn new(weight: f64, scale: SetScaling) -> Self { + Self { weight, scale } + } + + pub fn __getnewargs__(&self, py: Python) -> Py { + (self.weight, self.scale).into_py(py) + } + + pub fn __eq__(&self, py: Python, other: Py) -> bool { + if let Ok(other) = other.extract::(py) { + self == &other + } else { + false + } + } + + pub fn __repr__(&self, py: Python) -> PyResult> { + let fmt = "BasicHeuristic(weight={!r}, scale={!r})"; + Ok(PyString::new_bound(py, fmt) + .call_method1("format", (self.weight, self.scale))? + .into_py(py)) + } +} + +/// Define the characteristics of the lookahead heuristic. This is a sum of the physical distances +/// of every gate in the lookahead set, which is gates immediately after the front layer. +#[pyclass] +#[pyo3(module = "qiskit._accelerate.sabre", frozen)] +#[derive(Clone, Copy, PartialEq)] +pub struct LookaheadHeuristic { + /// The relative weight of this heuristic. Typically this is defined relative to the + /// :class:`.BasicHeuristic`, which generally has its weight set to 1.0. + pub weight: f64, + /// Number of gates to consider in the heuristic. + pub size: usize, + /// Dynamic scaling of the heuristic weight depending on the lookahead set. + pub scale: SetScaling, +} +#[pymethods] +impl LookaheadHeuristic { + #[new] + pub fn new(weight: f64, size: usize, scale: SetScaling) -> Self { + Self { + weight, + size, + scale, + } + } + + pub fn __getnewargs__(&self, py: Python) -> Py { + (self.weight, self.size, self.scale).into_py(py) + } + + pub fn __eq__(&self, py: Python, other: Py) -> bool { + if let Ok(other) = other.extract::(py) { + self == &other + } else { + false + } + } + + pub fn __repr__(&self, py: Python) -> PyResult> { + let fmt = "LookaheadHeuristic(weight={!r}, size={!r}, scale={!r})"; + Ok(PyString::new_bound(py, fmt) + .call_method1("format", (self.weight, self.size, self.scale))? + .into_py(py)) + } +} + +/// Define the characteristics of the "decay" heuristic. In this, each physical qubit has a +/// multiplier associated with it, beginning at 1.0, and has :attr:`increment` added to it each time +/// the qubit is involved in a swap. The final heuristic is calculated by multiplying all other +/// components by the maximum multiplier involved in a given swap. +#[pyclass] +#[pyo3(module = "qiskit._accelerate.sabre", frozen)] +#[derive(Clone, Copy, PartialEq)] +pub struct DecayHeuristic { + /// The amount to add onto the multiplier of a physical qubit when it is used. + pub increment: f64, + /// How frequently (in terms of swaps in the layer) to reset all qubit multipliers back to 1.0. + pub reset: usize, +} +#[pymethods] +impl DecayHeuristic { + #[new] + pub fn new(increment: f64, reset: usize) -> Self { + Self { increment, reset } + } + + pub fn __getnewargs__(&self, py: Python) -> Py { + (self.increment, self.reset).into_py(py) + } + + pub fn __eq__(&self, py: Python, other: Py) -> bool { + if let Ok(other) = other.extract::(py) { + self == &other + } else { + false + } + } + + pub fn __repr__(&self, py: Python) -> PyResult> { + let fmt = "DecayHeuristic(increment={!r}, reset={!r})"; + Ok(PyString::new_bound(py, fmt) + .call_method1("format", (self.increment, self.reset))? + .into_py(py)) + } +} + +/// A complete description of the heuristic that Sabre will use. See the individual elements for a +/// greater description. +#[pyclass] +#[pyo3(module = "qiskit._accelerate.sabre", frozen)] +#[derive(Clone, PartialEq)] +pub struct Heuristic { + pub basic: Option, + pub lookahead: Option, + pub decay: Option, + pub best_epsilon: f64, + pub attempt_limit: usize, +} + +#[pymethods] +impl Heuristic { + /// Construct a new Sabre heuristic. This can either be made directly of the desired + /// components, or you can make an empty heuristic and use the ``with_*`` methods to add + /// components to it. + /// + /// Args: + /// attempt_limit (int): the maximum number of swaps to attempt before using a fallback + /// "escape" mechanism to forcibly route a gate. Set this to ``None`` to entirely + /// disable the mechanism, but beware that it's possible (on large coupling maps with a + /// lookahead heuristic component) for Sabre to get stuck in an inescapable arbitrarily + /// deep local minimum of the heuristic. If this happens, and the escape mechanism is + /// disabled entirely, Sabre will enter an infinite loop. + /// best_epsilon (float): the floating-point epsilon to use when comparing scores to find + /// the best value. + #[new] + #[pyo3(signature = (basic=None, lookahead=None, decay=None, attempt_limit=1000, best_epsilon=1e-10))] + pub fn new( + basic: Option, + lookahead: Option, + decay: Option, + attempt_limit: Option, + best_epsilon: f64, + ) -> Self { + Self { + basic, + lookahead, + decay, + best_epsilon, + attempt_limit: attempt_limit.unwrap_or(usize::MAX), + } + } + + pub fn __getnewargs__(&self, py: Python) -> Py { + ( + self.basic, + self.lookahead, + self.decay, + self.attempt_limit, + self.best_epsilon, + ) + .into_py(py) + } + + /// Set the weight of the ``basic`` heuristic (the sum of distances of gates in the front + /// layer). This is often set to ``1.0``. You almost certainly should enable this part of the + /// heuristic, or it's highly unlikely that Sabre will be able to make any progress. + pub fn with_basic(&self, weight: f64, scale: SetScaling) -> Self { + Self { + basic: Some(BasicHeuristic { weight, scale }), + ..self.clone() + } + } + + /// Set the weight and extended-set size of the ``lookahead`` heuristic. The weight here + /// should typically be less than that of ``basic``. + pub fn with_lookahead(&self, weight: f64, size: usize, scale: SetScaling) -> Self { + Self { + lookahead: Some(LookaheadHeuristic { + weight, + size, + scale, + }), + ..self.clone() + } + } + + /// Set the multiplier increment and reset interval of the decay heuristic. The reset interval + /// must be non-zero. + pub fn with_decay(&self, increment: f64, reset: usize) -> PyResult { + if reset == 0 { + Err(PyValueError::new_err("decay reset interval cannot be zero")) + } else { + Ok(Self { + decay: Some(DecayHeuristic { increment, reset }), + ..self.clone() + }) + } + } + + pub fn __eq__(&self, py: Python, other: Py) -> bool { + if let Ok(other) = other.extract::(py) { + self == &other + } else { + false + } + } + + pub fn __repr__(&self, py: Python) -> PyResult> { + let fmt = "Heuristic(basic={!r}, lookahead={!r}, decay={!r}, attempt_limit={!r}, best_epsilon={!r})"; + Ok(PyString::new_bound(py, fmt) + .call_method1( + "format", + ( + self.basic, + self.lookahead, + self.decay, + self.attempt_limit, + self.best_epsilon, + ), + )? + .into_py(py)) + } +} diff --git a/crates/accelerate/src/sabre/layer.rs b/crates/accelerate/src/sabre/layer.rs index 8874a375935f..899321a96681 100644 --- a/crates/accelerate/src/sabre/layer.rs +++ b/crates/accelerate/src/sabre/layer.rs @@ -47,6 +47,11 @@ impl FrontLayer { } } + /// Number of gates currently stored in the layer. + pub fn len(&self) -> usize { + self.nodes.len() + } + /// View onto the mapping between qubits and their `(node, other_qubit)` pair. Index `i` /// corresponds to physical qubit `i`. pub fn qubits(&self) -> &[Option<(NodeIndex, PhysicalQubit)>] { @@ -77,11 +82,8 @@ impl FrontLayer { } /// Calculate the score _difference_ caused by this swap, compared to not making the swap. - #[inline] + #[inline(always)] pub fn score(&self, swap: [PhysicalQubit; 2], dist: &ArrayView2) -> f64 { - if self.is_empty() { - return 0.0; - } // At most there can be two affected gates in the front layer (one on each qubit in the // swap), since any gate whose closest path passes through the swapped qubit link has its // "virtual-qubit path" order changed, but not the total weight. In theory, we should @@ -96,18 +98,14 @@ impl FrontLayer { if let Some((_, c)) = self.qubits[b.index()] { total += dist[[a.index(), c.index()]] - dist[[b.index(), c.index()]] } - total / self.nodes.len() as f64 + total } /// Calculate the total absolute of the current front layer on the given layer. pub fn total_score(&self, dist: &ArrayView2) -> f64 { - if self.is_empty() { - return 0.0; - } self.iter() .map(|(_, &[a, b])| dist[[a.index(), b.index()]]) .sum::() - / self.nodes.len() as f64 } /// Apply a physical swap to the current layout data structure. @@ -181,10 +179,8 @@ impl ExtendedSet { } /// Calculate the score of applying the given swap, relative to not applying it. + #[inline(always)] pub fn score(&self, swap: [PhysicalQubit; 2], dist: &ArrayView2) -> f64 { - if self.is_empty() { - return 0.0; - } let [a, b] = swap; let mut total = 0.0; for other in self.qubits[a.index()].iter() { @@ -201,14 +197,12 @@ impl ExtendedSet { } total += dist[[a.index(), other.index()]] - dist[[b.index(), other.index()]]; } - total / self.len as f64 + total } /// Calculate the total absolute score of this set of nodes over the given layout. pub fn total_score(&self, dist: &ArrayView2) -> f64 { - if self.is_empty() { - return 0.0; - } + // Factor of two is to remove double-counting of each gate. self.qubits .iter() .enumerate() @@ -216,7 +210,7 @@ impl ExtendedSet { others.iter().map(move |b| dist[[a_index, b.index()]]) }) .sum::() - / (2.0 * self.len as f64) // Factor of two is to remove double-counting of each gate. + * 0.5 } /// Clear all nodes from the extended set. diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index a1e5e9ce6418..5ea568559119 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -24,11 +24,12 @@ use rayon::prelude::*; use crate::getenv_use_multiple_threads; use crate::nlayout::{NLayout, PhysicalQubit}; +use super::heuristic::Heuristic; use super::neighbor_table::NeighborTable; use super::route::{swap_map, swap_map_trial, RoutingTargetView}; use super::sabre_dag::SabreDAG; use super::swap_map::SwapMap; -use super::{Heuristic, NodeBlockResults, SabreResult}; +use super::{NodeBlockResults, SabreResult}; use crate::dense_layout::best_subset_inner; @@ -39,7 +40,7 @@ pub fn sabre_layout_and_routing( dag: &SabreDAG, neighbor_table: &NeighborTable, distance_matrix: PyReadonlyArray2, - heuristic: Heuristic, + heuristic: &Heuristic, max_iterations: usize, num_swap_trials: usize, num_random_trials: usize, @@ -129,7 +130,7 @@ pub fn sabre_layout_and_routing( fn layout_trial( target: &RoutingTargetView, dag: &SabreDAG, - heuristic: Heuristic, + heuristic: &Heuristic, seed: u64, max_iterations: usize, num_swap_trials: usize, diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 1229be16b723..287fdd743dfa 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +mod heuristic; mod layer; mod layout; mod neighbor_table; @@ -29,14 +30,6 @@ use neighbor_table::NeighborTable; use sabre_dag::SabreDAG; use swap_map::SwapMap; -#[pyclass] -#[derive(Clone, Copy)] -pub enum Heuristic { - Basic, - Lookahead, - Decay, -} - /// A container for Sabre mapping results. #[pyclass(module = "qiskit._accelerate.sabre")] #[derive(Clone, Debug)] @@ -117,7 +110,11 @@ impl BlockResult { pub fn sabre(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(route::sabre_routing))?; m.add_wrapped(wrap_pyfunction!(layout::sabre_layout_and_routing))?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/accelerate/src/sabre/route.rs b/crates/accelerate/src/sabre/route.rs index beb4cc3cf3b9..bef6d501b4aa 100644 --- a/crates/accelerate/src/sabre/route.rs +++ b/crates/accelerate/src/sabre/route.rs @@ -31,22 +31,13 @@ use rustworkx_core::token_swapper::token_swapper; use crate::getenv_use_multiple_threads; use crate::nlayout::{NLayout, PhysicalQubit}; +use super::heuristic::{BasicHeuristic, DecayHeuristic, Heuristic, LookaheadHeuristic, SetScaling}; use super::layer::{ExtendedSet, FrontLayer}; use super::neighbor_table::NeighborTable; use super::sabre_dag::SabreDAG; use super::swap_map::SwapMap; -use super::{BlockResult, Heuristic, NodeBlockResults, SabreResult}; +use super::{BlockResult, NodeBlockResults, SabreResult}; -/// Epsilon used in minimum-score calculations. -const BEST_EPSILON: f64 = 1e-10; -/// Size of lookahead window. -const EXTENDED_SET_SIZE: usize = 20; -/// Decay coefficient for penalizing serial swaps. -const DECAY_RATE: f64 = 0.001; -/// How often to reset all decay rates to 1. -const DECAY_RESET_INTERVAL: u8 = 5; -/// Weight of lookahead window compared to front_layer. -const EXTENDED_SET_WEIGHT: f64 = 0.5; /// Number of trials for control flow block swap epilogues. const SWAP_EPILOGUE_TRIALS: usize = 4; @@ -67,7 +58,7 @@ pub struct RoutingTargetView<'a> { struct RoutingState<'a, 'b> { target: &'a RoutingTargetView<'b>, dag: &'a SabreDAG, - heuristic: Heuristic, + heuristic: &'a Heuristic, /// Mapping of instructions (node indices) to swaps that precede them. out_map: HashMap>, /// Order of the instructions (node indices) in the problem DAG in the output. @@ -76,16 +67,17 @@ struct RoutingState<'a, 'b> { node_block_results: HashMap>, front_layer: FrontLayer, extended_set: ExtendedSet, + decay: &'a mut [f64], /// How many predecessors still need to be satisfied for each node index before it is at the /// front of the topological iteration through the nodes as they're routed. required_predecessors: &'a mut [u32], layout: NLayout, - /// Tracking for the 'decay' heuristic on each qubit. - qubits_decay: &'a mut [f64], - /// Reusable allocated storage space for choosing the best swap. This is owned outside of the - /// `choose_best_swap` function so that we don't need to reallocate and then re-grow the - /// collection on every entry. - swap_scratch: Vec<[PhysicalQubit; 2]>, + /// Reusable allocated storage space for accumulating and scoring swaps. This is owned as part + /// of the general state to avoid reallocation costs. + swap_scores: Vec<([PhysicalQubit; 2], f64)>, + /// Reusable allocated storage space for tracking the current best swaps. This is owned as + /// part of the general state to avoid reallocation costs. + best_swaps: Vec<[PhysicalQubit; 2]>, rng: Pcg64Mcg, seed: u64, } @@ -241,13 +233,19 @@ impl<'a, 'b> RoutingState<'a, 'b> { /// layer (and themselves). This uses `required_predecessors` as scratch space for efficiency, /// but returns it to the same state as the input on return. fn populate_extended_set(&mut self) { + let extended_set_size = + if let Some(LookaheadHeuristic { size, .. }) = self.heuristic.lookahead { + size + } else { + return; + }; let mut to_visit = self.front_layer.iter_nodes().copied().collect::>(); let mut decremented: IndexMap = IndexMap::with_hasher(ahash::RandomState::default()); let mut i = 0; let mut visit_now: Vec = Vec::new(); let dag = &self.dag; - while i < to_visit.len() && self.extended_set.len() < EXTENDED_SET_SIZE { + while i < to_visit.len() && self.extended_set.len() < extended_set_size { // Visit runs of non-2Q gates fully before moving on to children of 2Q gates. This way, // traversal order is a BFS of 2Q gates rather than of all gates. visit_now.push(to_visit[i]); @@ -335,61 +333,76 @@ impl<'a, 'b> RoutingState<'a, 'b> { /// Return the swap of two virtual qubits that produces the best score of all possible swaps. fn choose_best_swap(&mut self) -> [PhysicalQubit; 2] { - self.swap_scratch.clear(); - let mut min_score = f64::MAX; - // The decay heuristic is the only one that actually needs the absolute score. - let dist = &self.target.distance; - let absolute_score = match self.heuristic { - Heuristic::Decay => { - self.front_layer.total_score(dist) - + EXTENDED_SET_WEIGHT * self.extended_set.total_score(dist) + // Obtain all candidate swaps from the front layer. A coupling-map edge is a candidate + // swap if it involves at least one active qubit (i.e. it must affect the "basic" + // heuristic), and if it involves two active qubits, we choose the `swap[0] < swap[1]` form + // to make a canonical choice. + self.swap_scores.clear(); + for &phys in self.front_layer.iter_active() { + for &neighbor in self.target.neighbors[phys].iter() { + if neighbor > phys || !self.front_layer.is_active(neighbor) { + self.swap_scores.push(([phys, neighbor], 0.0)); + } } - _ => 0.0, - }; - for swap in obtain_swaps(&self.front_layer, self.target.neighbors) { - let score = match self.heuristic { - Heuristic::Basic => self.front_layer.score(swap, dist), - Heuristic::Lookahead => { - self.front_layer.score(swap, dist) - + EXTENDED_SET_WEIGHT * self.extended_set.score(swap, dist) + } + + let dist = &self.target.distance; + let mut absolute_score = 0.0; + + if let Some(BasicHeuristic { weight, scale }) = self.heuristic.basic { + let weight = match scale { + SetScaling::Constant => weight, + SetScaling::Size => { + if self.front_layer.is_empty() { + 0.0 + } else { + weight / (self.front_layer.len() as f64) + } } - Heuristic::Decay => { - self.qubits_decay[swap[0].index()].max(self.qubits_decay[swap[1].index()]) - * (absolute_score - + self.front_layer.score(swap, dist) - + EXTENDED_SET_WEIGHT * self.extended_set.score(swap, dist)) + }; + absolute_score += weight * self.front_layer.total_score(dist); + for (swap, score) in self.swap_scores.iter_mut() { + *score += weight * self.front_layer.score(*swap, dist); + } + } + + if let Some(LookaheadHeuristic { weight, scale, .. }) = self.heuristic.lookahead { + let weight = match scale { + SetScaling::Constant => weight, + SetScaling::Size => { + if self.extended_set.is_empty() { + 0.0 + } else { + weight / (self.extended_set.len() as f64) + } } }; - if score < min_score - BEST_EPSILON { - min_score = score; - self.swap_scratch.clear(); - self.swap_scratch.push(swap); - } else if (score - min_score).abs() < BEST_EPSILON { - self.swap_scratch.push(swap); + absolute_score += weight * self.extended_set.total_score(dist); + for (swap, score) in self.swap_scores.iter_mut() { + *score += weight * self.extended_set.score(*swap, dist); } } - *self.swap_scratch.choose(&mut self.rng).unwrap() - } -} -/// Return a set of candidate swaps that affect qubits in front_layer. -/// -/// For each virtual qubit in `front_layer`, find its current location on hardware and the physical -/// qubits in that neighborhood. Every swap on virtual qubits that corresponds to one of those -/// physical couplings is a candidate swap. -fn obtain_swaps<'a>( - front_layer: &'a FrontLayer, - neighbors: &'a NeighborTable, -) -> impl Iterator + 'a { - front_layer.iter_active().flat_map(move |&p| { - neighbors[p].iter().filter_map(move |&neighbor| { - if neighbor > p || !front_layer.is_active(neighbor) { - Some([p, neighbor]) - } else { - None + if let Some(DecayHeuristic { .. }) = self.heuristic.decay { + for (swap, score) in self.swap_scores.iter_mut() { + *score = (absolute_score + *score) + * self.decay[swap[0].index()].max(self.decay[swap[1].index()]); } - }) - }) + } + + let mut min_score = f64::INFINITY; + let epsilon = self.heuristic.best_epsilon; + for &(swap, score) in self.swap_scores.iter() { + if score - min_score < -epsilon { + min_score = score; + self.best_swaps.clear(); + self.best_swaps.push(swap); + } else if (score - min_score).abs() <= epsilon { + self.best_swaps.push(swap); + } + } + *self.best_swaps.choose(&mut self.rng).unwrap() + } } /// Run sabre swap on a circuit @@ -408,7 +421,7 @@ pub fn sabre_routing( dag: &SabreDAG, neighbor_table: &NeighborTable, distance_matrix: PyReadonlyArray2, - heuristic: Heuristic, + heuristic: &Heuristic, initial_layout: &NLayout, num_trials: usize, seed: Option, @@ -449,7 +462,7 @@ pub fn sabre_routing( pub fn swap_map( target: &RoutingTargetView, dag: &SabreDAG, - heuristic: Heuristic, + heuristic: &Heuristic, initial_layout: &NLayout, seed: Option, num_trials: usize, @@ -498,7 +511,7 @@ pub fn swap_map( pub fn swap_map_trial( target: &RoutingTargetView, dag: &SabreDAG, - heuristic: Heuristic, + heuristic: &Heuristic, initial_layout: &NLayout, seed: u64, ) -> (SabreResult, NLayout) { @@ -512,10 +525,11 @@ pub fn swap_map_trial( node_block_results: HashMap::with_capacity(dag.node_blocks.len()), front_layer: FrontLayer::new(num_qubits), extended_set: ExtendedSet::new(num_qubits), + decay: &mut vec![1.; num_qubits as usize], required_predecessors: &mut vec![0; dag.dag.node_count()], layout: initial_layout.clone(), - qubits_decay: &mut vec![1.; num_qubits as usize], - swap_scratch: Vec::new(), + swap_scores: Vec::with_capacity(target.coupling.edge_count()), + best_swaps: Vec::new(), rng: Pcg64Mcg::seed_from_u64(seed), seed, }; @@ -529,15 +543,14 @@ pub fn swap_map_trial( // Main logic loop; the front layer only becomes empty when all nodes have been routed. At // each iteration of this loop, we route either one or two gates. - let max_iterations_without_progress = 10 * num_qubits as usize; - let mut num_search_steps: u8 = 0; let mut routable_nodes = Vec::::with_capacity(2); + let mut num_search_steps = 0; while !state.front_layer.is_empty() { let mut current_swaps: Vec<[PhysicalQubit; 2]> = Vec::new(); // Swap-mapping loop. This is the main part of the algorithm, which we repeat until we // either successfully route a node, or exceed the maximum number of attempts. - while routable_nodes.is_empty() && current_swaps.len() <= max_iterations_without_progress { + while routable_nodes.is_empty() && current_swaps.len() <= state.heuristic.attempt_limit { let best_swap = state.choose_best_swap(); state.apply_swap(best_swap); current_swaps.push(best_swap); @@ -547,13 +560,15 @@ pub fn swap_map_trial( if let Some(node) = state.routable_node_on_qubit(best_swap[0]) { routable_nodes.push(node); } - num_search_steps += 1; - if num_search_steps >= DECAY_RESET_INTERVAL { - state.qubits_decay.fill(1.); - num_search_steps = 0; - } else { - state.qubits_decay[best_swap[0].index()] += DECAY_RATE; - state.qubits_decay[best_swap[1].index()] += DECAY_RATE; + if let Some(DecayHeuristic { increment, reset }) = state.heuristic.decay { + num_search_steps += 1; + if num_search_steps >= reset { + state.decay.fill(1.); + num_search_steps = 0; + } else { + state.decay[best_swap[0].index()] += increment; + state.decay[best_swap[1].index()] += increment; + } } } if routable_nodes.is_empty() { @@ -568,7 +583,9 @@ pub fn swap_map_trial( routable_nodes.push(force_routed); } state.update_route(&routable_nodes, current_swaps); - state.qubits_decay.fill(1.); + if state.heuristic.decay.is_some() { + state.decay.fill(1.); + } routable_nodes.clear(); } ( diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 2fb9a1890bd5..4ce94ecdb62f 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -35,11 +35,7 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit._accelerate.nlayout import NLayout -from qiskit._accelerate.sabre import ( - sabre_layout_and_routing, - Heuristic, - NeighborTable, -) +from qiskit._accelerate.sabre import sabre_layout_and_routing, Heuristic, NeighborTable, SetScaling from qiskit.transpiler.passes.routing.sabre_swap import _build_sabre_dag, _apply_sabre_result from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap @@ -393,12 +389,18 @@ def _inner_run(self, dag, coupling_map, starting_layouts=None): coupling_map.size(), original_qubit_indices, ) + heuristic = ( + Heuristic(attempt_limit=10 * coupling_map.size()) + .with_basic(1.0, SetScaling.Size) + .with_lookahead(0.5, 20, SetScaling.Size) + .with_decay(0.001, 5) + ) sabre_start = time.perf_counter() (initial_layout, final_permutation, sabre_result) = sabre_layout_and_routing( sabre_dag, neighbor_table, dist_matrix, - Heuristic.Decay, + heuristic, self.max_iterations, self.swap_trials, self.layout_trials, diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index acb23f39ab09..c8efaabea2bf 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -31,12 +31,7 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.utils.parallel import CPU_COUNT -from qiskit._accelerate.sabre import ( - sabre_routing, - Heuristic, - NeighborTable, - SabreDAG, -) +from qiskit._accelerate.sabre import sabre_routing, Heuristic, SetScaling, NeighborTable, SabreDAG from qiskit._accelerate.nlayout import NLayout logger = logging.getLogger(__name__) @@ -211,12 +206,25 @@ def run(self, dag): " This circuit cannot be routed to this device." ) - if self.heuristic == "basic": - heuristic = Heuristic.Basic + if isinstance(self.heuristic, Heuristic): + heuristic = self.heuristic + elif self.heuristic == "basic": + heuristic = Heuristic(attempt_limit=10 * num_dag_qubits).with_basic( + 1.0, SetScaling.Size + ) elif self.heuristic == "lookahead": - heuristic = Heuristic.Lookahead + heuristic = ( + Heuristic(attempt_limit=10 * num_dag_qubits) + .with_basic(1.0, SetScaling.Size) + .with_lookahead(0.5, 20, SetScaling.Size) + ) elif self.heuristic == "decay": - heuristic = Heuristic.Decay + heuristic = ( + Heuristic(attempt_limit=10 * num_dag_qubits) + .with_basic(1.0, SetScaling.Size) + .with_lookahead(0.5, 20, SetScaling.Size) + .with_decay(0.001, 5) + ) else: raise TranspilerError(f"Heuristic {self.heuristic} not recognized.") disjoint_utils.require_layout_isolated_to_component( From 68687d3efbd6e3c1f97ff9d4b14a9c969642bd56 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Wed, 31 Jul 2024 01:50:26 +0900 Subject: [PATCH 18/27] Update metadata of Primitives V2 (#12784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update metadata of primitives v2 * remove `shots` and add reno * add version * add shots to SamplerV2 metadata * udpate reno --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/primitives/backend_estimator_v2.py | 12 +++++++--- qiskit/primitives/backend_sampler_v2.py | 15 +++++++++--- qiskit/primitives/statevector_estimator.py | 6 +++-- qiskit/primitives/statevector_sampler.py | 7 ++++-- ...rimitive-v2-metadata-cf1226e2d6477688.yaml | 13 ++++++++++ .../primitives/test_backend_estimator_v2.py | 24 +++++++++++++++++++ .../primitives/test_backend_sampler_v2.py | 15 ++++++++++++ .../primitives/test_statevector_estimator.py | 23 +++++++++++++++--- .../primitives/test_statevector_sampler.py | 14 +++++++++++ 9 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 777375a30e4c..47352dd3de22 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -190,7 +190,7 @@ def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]: # reconstruct the result of pubs for i, pub_result in zip(lst, pub_results): results[i] = pub_result - return PrimitiveResult(results) + return PrimitiveResult(results, metadata={"version": 2}) def _run_pubs(self, pubs: list[EstimatorPub], shots: int) -> list[PubResult]: """Compute results for pubs that all require the same value of ``shots``.""" @@ -238,7 +238,6 @@ def _preprocess_pub(self, pub: EstimatorPub) -> _PreprocessedData: param_indices = np.fromiter(np.ndindex(param_shape), dtype=object).reshape(param_shape) bc_param_ind, bc_obs = np.broadcast_arrays(param_indices, observables) - # calculate expectation values for each pair of parameter value set and pauli param_obs_map = defaultdict(set) for index in np.ndindex(*bc_param_ind.shape): param_index = bc_param_ind[index] @@ -275,7 +274,14 @@ def _postprocess_pub( variances[index] += np.abs(coeff) * variance**0.5 stds = variances / np.sqrt(shots) data_bin = DataBin(evs=evs, stds=stds, shape=evs.shape) - return PubResult(data_bin, metadata={"target_precision": pub.precision}) + return PubResult( + data_bin, + metadata={ + "target_precision": pub.precision, + "shots": shots, + "circuit_metadata": pub.circuit.metadata, + }, + ) def _bind_and_add_measurements( self, diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index ff7a32580fe3..bac0bec5eaed 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -155,7 +155,7 @@ def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: # reconstruct the result of pubs for i, pub_result in zip(lst, pub_results): results[i] = pub_result - return PrimitiveResult(results) + return PrimitiveResult(results, metadata={"version": 2}) def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]: """Compute results for pubs that all require the same value of ``shots``.""" @@ -183,7 +183,12 @@ def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult end = start + bound.size results.append( self._postprocess_pub( - result_memory[start:end], shots, bound.shape, meas_info, max_num_bytes + result_memory[start:end], + shots, + bound.shape, + meas_info, + max_num_bytes, + pub.circuit.metadata, ) ) start = end @@ -197,6 +202,7 @@ def _postprocess_pub( shape: tuple[int, ...], meas_info: list[_MeasureInfo], max_num_bytes: int, + circuit_metadata: dict, ) -> SamplerPubResult: """Converts the memory data into an array of bit arrays with the shape of the pub.""" arrays = { @@ -213,7 +219,10 @@ def _postprocess_pub( meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - return SamplerPubResult(DataBin(**meas, shape=shape), metadata={}) + return SamplerPubResult( + DataBin(**meas, shape=shape), + metadata={"shots": shots, "circuit_metadata": circuit_metadata}, + ) def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index 722291bcf42f..c57f2c5b77d5 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -136,7 +136,7 @@ def run( return job def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]: - return PrimitiveResult([self._run_pub(pub) for pub in pubs]) + return PrimitiveResult([self._run_pub(pub) for pub in pubs], metadata={"version": 2}) def _run_pub(self, pub: EstimatorPub) -> PubResult: rng = np.random.default_rng(self._seed) @@ -162,4 +162,6 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: evs[index] = expectation_value data = DataBin(evs=evs, stds=stds, shape=evs.shape) - return PubResult(data, metadata={"precision": precision}) + return PubResult( + data, metadata={"target_precision": precision, "circuit_metadata": pub.circuit.metadata} + ) diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index 58980cb79f6e..78672e441dc4 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -171,7 +171,7 @@ def run( def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: results = [self._run_pub(pub) for pub in pubs] - return PrimitiveResult(results) + return PrimitiveResult(results, metadata={"version": 2}) def _run_pub(self, pub: SamplerPub) -> SamplerPubResult: circuit, qargs, meas_info = _preprocess_circuit(pub.circuit) @@ -197,7 +197,10 @@ def _run_pub(self, pub: SamplerPub) -> SamplerPubResult: meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - return SamplerPubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots}) + return SamplerPubResult( + DataBin(**meas, shape=pub.shape), + metadata={"shots": pub.shots, "circuit_metadata": pub.circuit.metadata}, + ) def _preprocess_circuit(circuit: QuantumCircuit): diff --git a/releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml b/releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml new file mode 100644 index 000000000000..570f5211b080 --- /dev/null +++ b/releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml @@ -0,0 +1,13 @@ +--- +features_primitives: + - | + The metadata of Primitives V2 implementations, i.e., :class:`.StatevectorSampler`, + :class:`.StatevectorEstimator`, :class:`.BackendSamplerV2` and :class:`.BackendEstimatorV2`, + has been updated to match that of IBM quantum devices. + + * ``version`` and ``circuit_metadata`` are added for all V2 implementations + * ``shots`` is added for :class:`.BackendSamplerV2` and :class:`.BackendEstimatorV2` + * ``precision`` is renamed with ``target_precision`` for :class:`.StatevectorEstimator` + + Note that metadata of :class:`.StatevectorEstimator` does not have ``shots`` because + the class computes expectation values with :class:`.Statevector` and shots are not used. diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 319fd846ee95..ad6b4c6abfdb 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -474,6 +474,30 @@ def test_iter_pub(self): np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol) np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733], rtol=self._rtol) + def test_metadata(self): + """Test for metadata""" + qc = QuantumCircuit(2) + qc2 = QuantumCircuit(2) + qc2.metadata = {"a": 1} + backend = BasicSimulator() + estimator = BackendEstimatorV2(backend=backend) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + op = SparsePauliOp("ZZ").apply_layout(qc.layout) + op2 = SparsePauliOp("ZZ").apply_layout(qc2.layout) + result = estimator.run([(qc, op), (qc2, op2)], precision=0.1).result() + + self.assertEqual(len(result), 2) + self.assertEqual(result.metadata, {"version": 2}) + self.assertEqual( + result[0].metadata, + {"target_precision": 0.1, "shots": 100, "circuit_metadata": qc.metadata}, + ) + self.assertEqual( + result[1].metadata, + {"target_precision": 0.1, "shots": 100, "circuit_metadata": qc2.metadata}, + ) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 632ed1984d2d..6e899dca58f9 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -748,6 +748,21 @@ def test_iter_pub(self): self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + def test_metadata(self): + """Test for metadata""" + qc = QuantumCircuit(2) + qc.measure_all() + qc2 = qc.copy() + qc2.metadata = {"a": 1} + backend = BasicSimulator() + sampler = BackendSamplerV2(backend=backend) + result = sampler.run([(qc, None, 10), (qc2, None, 20)]).result() + + self.assertEqual(len(result), 2) + self.assertEqual(result.metadata, {"version": 2}) + self.assertEqual(result[0].metadata, {"shots": 10, "circuit_metadata": qc.metadata}) + self.assertEqual(result[1].metadata, {"shots": 20, "circuit_metadata": qc2.metadata}) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 1ed2d42e0e3b..4eaa70e07a07 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -129,7 +129,7 @@ def test_run_single_circuit_observable(self): self.subTest(f"{val}") result = est.run([(qc, op, val)]).result() np.testing.assert_allclose(result[0].data.evs, target) - self.assertEqual(result[0].metadata["precision"], 0) + self.assertEqual(result[0].metadata["target_precision"], 0) with self.subTest("One parameter"): param = Parameter("x") @@ -145,7 +145,7 @@ def test_run_single_circuit_observable(self): self.subTest(f"{val}") result = est.run([(qc, op, val)]).result() np.testing.assert_allclose(result[0].data.evs, target) - self.assertEqual(result[0].metadata["precision"], 0) + self.assertEqual(result[0].metadata["target_precision"], 0) with self.subTest("More than one parameter"): qc = self.psi[0] @@ -162,7 +162,7 @@ def test_run_single_circuit_observable(self): self.subTest(f"{val}") result = est.run([(qc, op, val)]).result() np.testing.assert_allclose(result[0].data.evs, target) - self.assertEqual(result[0].metadata["precision"], 0) + self.assertEqual(result[0].metadata["target_precision"], 0) def test_run_1qubit(self): """Test for 1-qubit cases""" @@ -290,6 +290,23 @@ def test_iter_pub(self): np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733]) np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733]) + def test_metadata(self): + """Test for metadata""" + qc = QuantumCircuit(2) + qc2 = QuantumCircuit(2) + qc2.metadata = {"a": 1} + estimator = StatevectorEstimator() + result = estimator.run([(qc, "ZZ"), (qc2, "ZZ")], precision=0.1).result() + + self.assertEqual(len(result), 2) + self.assertEqual(result.metadata, {"version": 2}) + self.assertEqual( + result[0].metadata, {"target_precision": 0.1, "circuit_metadata": qc.metadata} + ) + self.assertEqual( + result[1].metadata, {"target_precision": 0.1, "circuit_metadata": qc2.metadata} + ) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index 1f84c072564d..a782aafaeaf5 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -648,6 +648,20 @@ def test_iter_pub(self): self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + def test_metadata(self): + """Test for metadata""" + qc = QuantumCircuit(2) + qc.measure_all() + qc2 = qc.copy() + qc2.metadata = {"a": 1} + sampler = StatevectorSampler() + result = sampler.run([(qc, None, 10), (qc2, None, 20)]).result() + + self.assertEqual(len(result), 2) + self.assertEqual(result.metadata, {"version": 2}) + self.assertEqual(result[0].metadata, {"shots": 10, "circuit_metadata": qc.metadata}) + self.assertEqual(result[1].metadata, {"shots": 20, "circuit_metadata": qc2.metadata}) + if __name__ == "__main__": unittest.main() From b75c2e093ebd4417906edfb159e557f3585b7c4e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Jul 2024 03:26:51 -0400 Subject: [PATCH 19/27] Add 100q QV circuit to PGO scripts (#12858) To get better coverage for PGO, including more examples of two qubit synthesis, this commit adds a new PGO script to the benchmarks for running a 100q quantum volume circuit. This provides a different dense circuit than the other 100q circuits we were running. At the time the pgo scripts were added this was excluded because it was too slow at the time. However for the upcoming 1.2 release the transpile time has sped up a bit so this isn't such a concern anymore as the local transpile time is now ~2min which seems within the threshold for pgo time. --- tools/pgo_scripts/test_utility_scale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/pgo_scripts/test_utility_scale.py b/tools/pgo_scripts/test_utility_scale.py index 0e9e94655692..4c2d44caac2a 100755 --- a/tools/pgo_scripts/test_utility_scale.py +++ b/tools/pgo_scripts/test_utility_scale.py @@ -18,6 +18,7 @@ from qiskit import qasm2 from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import CouplingMap +from qiskit.circuit.library import QuantumVolume from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager QASM_DIR = os.path.join( @@ -78,12 +79,11 @@ def _main(): strict=False, ) qaoa_circ.name = "qaoa_barabasi_albert_N100_3reps" - # Uncomment when this is fast enough to run during release builds - # qv_circ = QuantumVolume(100, seed=123456789) - # qv_circ.measure_all() - # qv_circ.name = "QV1267650600228229401496703205376" + qv_circ = QuantumVolume(100, seed=123456789) + qv_circ.measure_all() + qv_circ.name = "QV1267650600228229401496703205376" for pm in [cz_pm, ecr_pm, cx_pm]: - for circ in [qft_circ, square_heisenberg_circ, qaoa_circ]: + for circ in [qft_circ, square_heisenberg_circ, qaoa_circ, qv_circ]: print(f"Compiling: {circ.name}") pm.run(circ) From f9fcd1d0fd958ba7eb1bee380a26276776e086c1 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:27:01 -0400 Subject: [PATCH 20/27] Initial: Make ImportOneCell `new` method public. (#12860) --- crates/circuit/src/imports.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 153b66392083..47b29cc9c270 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -28,7 +28,7 @@ pub struct ImportOnceCell { } impl ImportOnceCell { - const fn new(module: &'static str, object: &'static str) -> Self { + pub const fn new(module: &'static str, object: &'static str) -> Self { Self { module, object, From a6ec8b9ad4f234ec8dad91cff1060f7e15ad2237 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 31 Jul 2024 12:53:40 +0200 Subject: [PATCH 21/27] Deprecate BackendV1 and `qiskit.providers.models` (#12629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * reno * Apply suggestions from code review * deprecate qobj and assemble * reno * first attempt on Fake1QV2 * deprecate Fake1Q * Fake1QV2 should not have two-qubit gates Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * test.python.circuit.test_parameters.TestParameters * test.python.providers.fake_provider.test_fake_backends.FakeBackendsTest * test.python.providers.test_fake_backends.TestFakeBackends * test.python.result.test_mitigators * test.python.transpiler.test_preset_passmanagers * test.python.transpiler.test_target * test.python.transpiler.test_unitary_synthesis * test.python.transpiler.test_vf2_layout * test.python.transpiler.test_vf2_post_layout * test/python/visualization/test_circuit_latex * test.python.transpiler.test_sabre_layout * test.python.transpiler.test_sabre_layout * test.python.transpiler.test_pulse_gate_pass * test.python.scheduler.test_basic_scheduler.TestBasicSchedule * test.python.pulse.test_transforms * test.python.pulse.test_schedule * test.python.pulse.test_macros * test.python.pulse.test_instruction_schedule_map * test.python.pulse.test_block * test.python.circuit.test_scheduled_circuit * test.python.transpiler.test_calibrationbuilder * test.python.providers.test_backendconfiguration * test.python.compiler.test_transpiler * test.python.transpiler.test_passmanager_run * test.python.transpiler.test_passmanager_config.TestPassManagerConfig.test_from_backend_and_user * test.python.transpiler.test_passmanager_config * test.python.primitives.test_backend_estimator.TestBackendEstimator * test.python.circuit.test_scheduled_circuit.TestScheduledCircuit.test_schedule_circuit_when_backend_tells_dt * test.python.circuit.test_scheduled_circuit * test.python.transpiler.test_vf2_layout * shallow deprecation of assemble * test.python.compiler.test_disassembler * fakebackend pulse * test.python.circuit.test_parameters * PulseQobjInstruction is used by GenericBackendV2 * test.python.scheduler.test_basic_scheduler * test.python.result.test_result * test.python.pulse.test_calibration_entries * test.python.compiler.test_assembler * test.python.transpiler.test_star_prerouting * test.python.pulse.test_instruction_schedule_map * test.python.providers.basic_provider.test_basic_simulator * test.python.primitives.test_backend_sampler_v2 * test.python.compiler.test_disassembler * test.python.compiler.test_compiler * test.python.circuit.test_scheduled_circuit * test.python.providers.test_fake_backends * test.python.circuit.test_unitary * test.python.transpiler.test_sabre_swap * test.python.providers.fake_provider.test_fake_backends * Aer using Provider ABC * aer warnings * reno * another pass on reno * test.python.pulse * test.python.compiler.test_compiler * add module to fiterwarning * test.python.compiler.test_transpiler * fixing obscure expcetion handleing for comparison * test.python.transpiler.test_stochastic_swap test.python.transpiler.test_sabre_swap * test.python.transpiler.test_echo_rzx_weyl_decomposition * test.python.transpiler.test_instruction_durations * test.python.providers.test_backendproperties * test.python.qpy.test_circuit_load_from_qpy * test.python.providers.test_pulse_defaults * test.python.primitives.test_backend_sampler_v2 * test.python.primitives.test_backend_sampler * test.python.compiler.test_scheduler * test/python/compiler/test_scheduler.py * test.python.compiler.test_disassembler * test.python.compiler.test_assembler * test.python.compiler.test_sequencer * test.python.compiler.test_transpiler * test.python.primitives.test_primitive * better depreaction handleling from Aer * test.python.primitives.test_backend_estimator_v2 * test.python.compiler.test_compiler * ignore warnings, because the output is otherwise very verbose * ignore to avoid fludding the CI log * seeding all GenericBackendV2 * test.python.visualization.test_gate_map * deprecation warnings: once * default * default * BackendConfiguration * PulseBackendConfiguration * QasmBackendConfiguration * QasmBackendConfiguration and adjust stacklevel * UchannelLO * GateConfig * test.python.providers.test_fake_backends.TestFakeBackends * test.python.providers.test_fake_backends * test.python.compiler.test_transpiler * remove catch * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * remove catch * new deprecate warning message * lint qiskit/assembler/assemble_circuits.py * concurrency warnings * ignore aer warnings * Update test/python/providers/fake_provider/test_fake_backends.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/circuit/test_parameters.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/providers/models/pulsedefaults.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/providers/fake_provider/test_fake_backends.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/python/providers/fake_provider/test_generic_backend_v2.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * lint * https://github.com/Qiskit/qiskit/pull/12649#discussion_r1686717954 * https://github.com/Qiskit/qiskit/pull/12649#discussion_r1686717732 * Update test/python/transpiler/test_sabre_swap.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/providers/models/pulsedefaults.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * ignore Treating CircuitInstruction... * another unnecessary catch from aer * another unnecessary catch from aer, again * removing more unnecesary catches * less lines * seeding * remove those comments * test.python.compiler.test_transpiler.TestTranspile.test_scheduling_timing_constraints * tokyo cmap * test.python.circuit.test_scheduled_circuit.TestScheduledCircuit.test_schedule_circuit_when_backend_tells_dt * comment in test_sequencer.py * test.python.compiler.test_transpiler.TestPostTranspileIntegration.test_qasm3_output * Update test/python/primitives/test_primitive.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * test/python/providers/test_backendconfiguration.py:30 * test.python.primitives.test_backend_sampler * restore the assertWarns, waiting for #12818 * use legacy_cmaps instead of explict coupling maps * use more legacy_map * KYOTO_CMAP * more legacy_cmap * https://github.com/Qiskit/qiskit/issues/12832 * test.python.transpiler.test_vf2_post_layout * comma * https://github.com/Qiskit/qiskit/pull/12660#discussion_r1694781920 * do not deprecate BackendV2Converter yet * test.python.providers.test_faulty_backend * test.python.compiler.test_transpiler.TestTranspile * cleaning up warnings * no deprecation in test/python/compiler/test_transpiler.py * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * reno lint * adding models in reno * black * test.python.transpiler.test_preset_passmanagers * seeding GenericBackendV2 * less Fake5QV1 * test.python.transpiler.test_sabre_layout * test.python.transpiler.test_pulse_gate_pass * test.python.pulse.test_builder * test.python.primitives.test_backend_sampler_v2 * test.python.primitives.test_backend_sampler_v2 * test.python.primitives.test_backend_estimator_v2 * test.python.compiler.test_transpiler * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * test.python.primitives.test_backend_estimator_v2.TestBackendEstimatorV2 * test.python.primitives.test_backend_estimator_v2 * Fix expected_regex in transpile tests * Fix missing assertions in: * test.python.circuit.test_scheduled_circuit * test.python.compiler.test_sequencer * test.python.circuit.test_parameters * test.python.primitives.test_backend_sampler_v2 * test.python.primitives.test_backend_sampler * Fix line-too-long lint complaints --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Elena Peña Tapia --- qiskit/circuit/add_control.py | 2 +- qiskit/compiler/transpiler.py | 26 +- qiskit/providers/backend.py | 11 +- qiskit/providers/backend_compat.py | 11 +- .../basic_provider/basic_simulator.py | 44 +- .../fake_provider/fake_qasm_backend.py | 6 +- qiskit/providers/models/__init__.py | 11 + .../providers/models/backendconfiguration.py | 50 +- qiskit/providers/models/backendproperties.py | 11 + .../preset_passmanagers/__init__.py | 4 +- .../generate_preset_pass_manager.py | 21 +- qiskit/transpiler/target.py | 16 +- qiskit/utils/deprecation.py | 5 +- .../notes/backendv1-d0d0642ed38fed3c.yaml | 14 + test/python/circuit/test_parameters.py | 7 +- test/python/circuit/test_scheduled_circuit.py | 180 ++++- test/python/compiler/test_sequencer.py | 42 +- test/python/compiler/test_transpiler.py | 128 ++-- .../primitives/test_backend_estimator_v2.py | 357 ++++++++- .../python/primitives/test_backend_sampler.py | 39 +- .../primitives/test_backend_sampler_v2.py | 719 +++++++++++++++++- test/python/providers/test_fake_backends.py | 77 +- test/python/providers/test_faulty_backend.py | 57 +- test/python/pulse/test_builder.py | 24 +- .../transpiler/test_calibrationbuilder.py | 8 +- .../transpiler/test_gates_in_basis_pass.py | 10 +- .../python/transpiler/test_passmanager_run.py | 10 +- .../transpiler/test_preset_passmanagers.py | 110 ++- .../python/transpiler/test_pulse_gate_pass.py | 101 ++- test/python/transpiler/test_sabre_layout.py | 20 +- test/python/transpiler/test_sabre_swap.py | 2 +- .../transpiler/test_unitary_synthesis.py | 15 +- .../python/transpiler/test_vf2_post_layout.py | 138 +++- .../visualization/test_circuit_latex.py | 11 +- test/utils/base.py | 9 + 35 files changed, 1967 insertions(+), 329 deletions(-) create mode 100644 releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 2b374d54c455..98c9b4d4e452 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -73,7 +73,7 @@ def control( ) -> ControlledGate: """Return controlled version of gate using controlled rotations. This function first checks the name of the operation to see if it knows of a method from which - to generate a controlled version. Currently these are `x`, `rx`, `ry`, and `rz`. + to generate a controlled version. Currently, these are ``x``, ``rx``, ``ry``, and ``rz``. If a method is not directly known, it calls the unroller to convert to `u1`, `u3`, and `cx` gates. diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index adf60ca91e56..fd93fde5989f 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -315,10 +315,28 @@ def callback_func(**kwargs): optimization_level = config.get("transpile_optimization_level", 2) if backend is not None and getattr(backend, "version", 0) <= 1: - # This is a temporary conversion step to allow for a smoother transition - # to a fully target-based transpiler pipeline while maintaining the behavior - # of `transpile` with BackendV1 inputs. - backend = BackendV2Converter(backend) + warnings.warn( + "The `transpile` function will stop supporting inputs of " + f"type `BackendV1` ( {backend} ) in the `backend` parameter in a future " + "release no earlier than 2.0. `BackendV1` is deprecated and implementations " + "should move to `BackendV2`.", + category=DeprecationWarning, + stacklevel=2, + ) + with warnings.catch_warnings(): + # This is a temporary conversion step to allow for a smoother transition + # to a fully target-based transpiler pipeline while maintaining the behavior + # of `transpile` with BackendV1 inputs. + # TODO BackendV1 is deprecated and this path can be + # removed once it gets removed: + # https://github.com/Qiskit/qiskit/pull/12850 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".+qiskit\.providers\.backend_compat\.BackendV2Converter.+", + module="qiskit", + ) + backend = BackendV2Converter(backend) if ( scheduling_method is not None diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 931dbed479ec..6d9ed51043ec 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -23,6 +23,7 @@ from qiskit.providers.provider import Provider from qiskit.providers.models.backendstatus import BackendStatus from qiskit.circuit.gate import Instruction +from qiskit.utils import deprecate_func class Backend: @@ -43,7 +44,7 @@ class BackendV1(Backend, ABC): This abstract class is to be used for Backend objects. There are several classes of information contained in a Backend. The first are the attributes of the class itself. These should be used to - defined the immutable characteristics of the backend. The ``options`` + define the immutable characteristics of the backend. The ``options`` attribute of the backend is used to contain the dynamic user configurable options of the backend. It should be used more for runtime options that configure how the backend is used. For example, something like a @@ -71,6 +72,14 @@ class BackendV1(Backend, ABC): version = 1 + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="If the backend only encapsulates a hardware description, " + "consider constructing a Target directly. If it is part of a provider " + "that gives access to execution, consider using Primitives instead. " + "Alternatively, consider moving to BackendV2 (see https://qisk.it/backendV1-to-V2).", + ) def __init__(self, configuration, provider=None, **fields): """Initialize a backend class diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index e567c330a958..f554897d34c4 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -390,10 +390,13 @@ def __init__( self._properties = None self._defaults = None - if hasattr(self._backend, "properties"): - self._properties = self._backend.properties() - if hasattr(self._backend, "defaults"): - self._defaults = self._backend.defaults() + with warnings.catch_warnings(): + # The class QobjExperimentHeader is deprecated + warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") + if hasattr(self._backend, "properties"): + self._properties = self._backend.properties() + if hasattr(self._backend, "defaults"): + self._defaults = self._backend.defaults() self._target = None self._name_mapping = name_mapping diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index 1d1205294677..32666c57f184 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -236,24 +236,32 @@ def configuration(self) -> BackendConfiguration: for name in self.target.operation_names ] - self._configuration = BackendConfiguration( - backend_name=self.name, - backend_version=self.backend_version, - n_qubits=self.num_qubits, - basis_gates=self.target.operation_names, - gates=gates, - local=True, - simulator=True, - conditional=True, - open_pulse=False, - memory=True, - # This max_shots is used by the assembler, setting it to 0 - # to maintain the behavior from the previous implementation. - # Not related to the actual shots set in the backend options - max_shots=0, - coupling_map=None, - description="A python simulator for quantum experiments", - ) + with warnings.catch_warnings(): + # TODO Provider models are deprecated + # https://github.com/Qiskit/qiskit/issues/12843 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".+qiskit\.providers\.models\.backendconfiguration\..+", + ) + self._configuration = BackendConfiguration( + backend_name=self.name, + backend_version=self.backend_version, + n_qubits=self.num_qubits, + basis_gates=self.target.operation_names, + gates=gates, + local=True, + simulator=True, + conditional=True, + open_pulse=False, + memory=True, + # This max_shots is used by the assembler, setting it to 0 + # to maintain the behavior from the previous implementation. + # Not related to the actual shots set in the backend options + max_shots=0, + coupling_map=None, + description="A python simulator for quantum experiments", + ) return self._configuration @classmethod diff --git a/qiskit/providers/fake_provider/fake_qasm_backend.py b/qiskit/providers/fake_provider/fake_qasm_backend.py index 55dad4aff601..7ad7222f7907 100644 --- a/qiskit/providers/fake_provider/fake_qasm_backend.py +++ b/qiskit/providers/fake_provider/fake_qasm_backend.py @@ -16,6 +16,7 @@ import json import os +import warnings from qiskit.exceptions import QiskitError from qiskit.providers.models import BackendProperties, QasmBackendConfiguration @@ -61,7 +62,10 @@ def _set_props_from_json(self): raise QiskitError("No properties file has been defined") props = self._load_json(self.props_filename) decode_backend_properties(props) - self._properties = BackendProperties.from_dict(props) + with warnings.catch_warnings(): + # This raises the BackendProperties deprecation warning internally + warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") + self._properties = BackendProperties.from_dict(props) def _load_json(self, filename): with open(os.path.join(self.dirname, filename)) as f_json: diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index bf90a9d16c0e..58fafed92986 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -38,6 +38,7 @@ GateProperties Nduv """ +import warnings from .backendconfiguration import ( BackendConfiguration, @@ -50,3 +51,13 @@ from .backendstatus import BackendStatus from .jobstatus import JobStatus from .pulsedefaults import PulseDefaults, Command + + +warnings.warn( + "qiskit.providers.models is deprecated since Qiskit 1.2 and will be removed in Qiskit 2.0." + "With the removal of Qobj, there is no need for these schema-conformant objects. If you still need" + "to use them, it could be because you are using a BackendV1, which is also deprecated in favor" + "of BackendV2", + DeprecationWarning, + 2, +) diff --git a/qiskit/providers/models/backendconfiguration.py b/qiskit/providers/models/backendconfiguration.py index ebd0a6d9bbb6..a50745c9572c 100644 --- a/qiskit/providers/models/backendconfiguration.py +++ b/qiskit/providers/models/backendconfiguration.py @@ -26,6 +26,7 @@ DriveChannel, MeasureChannel, ) +from qiskit.utils import deprecate_func class GateConfig: @@ -38,6 +39,15 @@ class GateConfig: and CX. """ + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` are part " + "of the deprecated `BackendV1` workflow and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + stacklevel=3, + ) def __init__( self, name, @@ -141,6 +151,14 @@ class UchannelLO: scale: Scale factor for qubit frequency. """ + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` are part " + "of the deprecated `BackendV1` workflow and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + ) def __init__(self, q, scale): """Initialize a UchannelLOSchema object @@ -211,6 +229,15 @@ class QasmBackendConfiguration: _data = {} + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` are part " + "of the deprecated `BackendV1` workflow and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + stacklevel=3, + ) def __init__( self, backend_name, @@ -491,9 +518,19 @@ def __contains__(self, item): class BackendConfiguration(QasmBackendConfiguration): - """Backwards compat shim representing an abstract backend configuration.""" + """Backwards compatibility shim representing an abstract backend configuration.""" - pass + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` are part " + "of the deprecated `BackendV1` workflow and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + stacklevel=3, + ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class PulseBackendConfiguration(QasmBackendConfiguration): @@ -501,6 +538,15 @@ class PulseBackendConfiguration(QasmBackendConfiguration): about the set up of the device which can be useful for building Pulse programs. """ + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` are part " + "of the deprecated `BackendV1` workflow and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + stacklevel=3, + ) def __init__( self, backend_name: str, diff --git a/qiskit/providers/models/backendproperties.py b/qiskit/providers/models/backendproperties.py index 332aac7c5edd..75e7cd18d03a 100644 --- a/qiskit/providers/models/backendproperties.py +++ b/qiskit/providers/models/backendproperties.py @@ -18,6 +18,7 @@ import dateutil.parser from qiskit.providers.exceptions import BackendPropertyError +from qiskit.utils import deprecate_func from qiskit.utils.units import apply_prefix PropertyT = Tuple[Any, datetime.datetime] @@ -172,6 +173,15 @@ class BackendProperties: _data = {} + @deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="The models in ``qiskit.providers.models`` and related objects are part " + "of the deprecated `BackendV1` workflow, and no longer necessary for `BackendV2`. If a user " + "workflow requires these representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", + stacklevel=3, + ) def __init__( self, backend_name, backend_version, last_update_date, qubits, gates, general, **kwargs ): @@ -248,6 +258,7 @@ def from_dict(cls, data): qubits.append(nduvs) gates = [GateProperties.from_dict(x) for x in in_data.pop("gates")] general = [Nduv.from_dict(x) for x in in_data.pop("general")] + return cls( backend_name, backend_version, last_update_date, qubits, gates, general, **in_data ) diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 6092573876f1..cad512c07a52 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -57,12 +57,12 @@ .. autofunction:: generate_scheduling .. currentmodule:: qiskit.transpiler.preset_passmanagers """ - +from .generate_preset_pass_manager import generate_preset_pass_manager from .level0 import level_0_pass_manager from .level1 import level_1_pass_manager from .level2 import level_2_pass_manager from .level3 import level_3_pass_manager -from .generate_preset_pass_manager import generate_preset_pass_manager + __all__ = [ "level_0_pass_manager", diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index bdbac42c8055..a9a9a5e2f029 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -15,6 +15,7 @@ """ import copy +import warnings from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping @@ -254,6 +255,14 @@ def generate_preset_pass_manager( # This is a temporary conversion step to allow for a smoother transition # to a fully target-based transpiler pipeline while maintaining the behavior # of `transpile` with BackendV1 inputs. + warnings.warn( + "The `generate_preset_pass_manager` function will stop supporting inputs of " + f"type `BackendV1` ( {backend} ) in the `backend` parameter in a future " + "release no earlier than 2.0. `BackendV1` is deprecated and implementations " + "should move to `BackendV2`.", + category=DeprecationWarning, + stacklevel=2, + ) backend = BackendV2Converter(backend) # Check if a custom inst_map was specified before overwriting inst_map @@ -331,7 +340,17 @@ def generate_preset_pass_manager( if timing_constraints is None: timing_constraints = target.timing_constraints() if backend_properties is None: - backend_properties = target_to_backend_properties(target) + with warnings.catch_warnings(): + # TODO this approach (target-to-properties) is going to be removed soon (1.3) in favor + # of backend-to-target approach + # https://github.com/Qiskit/qiskit/pull/12850 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".+qiskit\.transpiler\.target\.target_to_backend_properties.+", + module="qiskit", + ) + backend_properties = target_to_backend_properties(target) # Parse non-target dependent pm options initial_layout = _parse_initial_layout(initial_layout) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 001e8020962b..da4a44a8ee02 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -20,6 +20,7 @@ from __future__ import annotations import itertools +import warnings from typing import Optional, List, Any from collections.abc import Mapping @@ -55,6 +56,7 @@ # full target from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import from qiskit.providers.models.backendproperties import BackendProperties +from qiskit.utils import deprecate_func logger = logging.getLogger(__name__) @@ -1164,6 +1166,15 @@ def from_configuration( Mapping.register(Target) +@deprecate_func( + since="1.2", + removal_timeline="in the 2.0 release", + additional_msg="This method is used to build an element from the deprecated " + "``qiskit.providers.models`` module. These models are part of the deprecated `BackendV1` " + "workflow and no longer necessary for `BackendV2`. If a user workflow requires these " + "representations it likely relies on deprecated functionality and " + "should be updated to use `BackendV2`.", +) def target_to_backend_properties(target: Target): """Convert a :class:`~.Target` object into a legacy :class:`~.BackendProperties`""" @@ -1242,6 +1253,9 @@ def target_to_backend_properties(target: Target): if gates or qubits: properties_dict["gates"] = gates properties_dict["qubits"] = qubits - return BackendProperties.from_dict(properties_dict) + with warnings.catch_warnings(): + # This raises BackendProperties internally + warnings.filterwarnings("ignore", category=DeprecationWarning) + return BackendProperties.from_dict(properties_dict) else: return None diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 294ea9414924..aebea233282e 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -29,6 +29,7 @@ def deprecate_func( package_name: str = "qiskit", removal_timeline: str = "no earlier than 3 months after the release date", is_property: bool = False, + stacklevel: int = 2, ): """Decorator to indicate a function has been deprecated. @@ -50,7 +51,7 @@ def deprecate_func( is_property: If the deprecated function is a `@property`, set this to True so that the generated message correctly describes it as such. (This isn't necessary for property setters, as their docstring is ignored by Python.) - + stacklevel: Stack level passed to :func:`warnings.warn`. Returns: Callable: The decorated callable. """ @@ -92,7 +93,7 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - warnings.warn(msg, category=category, stacklevel=2) + warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending) diff --git a/releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml b/releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml new file mode 100644 index 000000000000..3b19a7ab9a2c --- /dev/null +++ b/releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml @@ -0,0 +1,14 @@ +--- +deprecations_providers: + - | + The :class:`.BackendV1` class is deprecated and it will be removed not earlier than the next major release. + There are several migration paths available depending on the main purpose of the backend object:: + + - To expose backend information with no access to execution (just a hardware description), consider constructing a :class:`.Target` directly. + - To provides access to execution capabilities, consider relying on the primitives interfaces instead. + - Alternatively, to continue providing simultaneous :class:`.Target` (hardware information) and ``run`` (execution) capabilities, consider moving to :class:`.BackendV2` (see ). + + - | + The models in :mod:`qiskit.providers.models` are part of the deprecated :class:`.BackendV1` workflow + and no longer necessary for :class:`.BackendV2`. If a user workflow requires these representations it + likely relies on deprecated functionality and should be updated to use :class:`.BackendV2`. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c2df1a423ec4..ad336a0ad6bb 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1093,7 +1093,12 @@ def test_transpile_across_optimization_levelsV1(self, opt_level): qc.measure(range(5 - 1), range(5 - 1)) with self.assertWarns(DeprecationWarning): backend = Fake5QV1() - transpile(qc, backend, optimization_level=opt_level) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpile(qc, backend, optimization_level=opt_level) @data(0, 1, 2, 3) def test_transpile_across_optimization_levels(self, opt_level): diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index c934b5b5e04c..ce5cd8213105 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -78,13 +78,18 @@ def test_schedule_circuit_when_transpile_option_tells_dt(self): qc.delay(100, 0, unit="ns") # 450[dt] qc.h(0) qc.h(1) - sc = transpile( - qc, - self.backend_without_dt, - scheduling_method="alap", - dt=self.dt, - layout_method="trivial", - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile( + qc, + self.backend_without_dt, + scheduling_method="alap", + dt=self.dt, + layout_method="trivial", + ) self.assertEqual(sc.duration, 450546) self.assertEqual(sc.unit, "dt") self.assertEqual(sc.data[0].operation.name, "delay") @@ -104,9 +109,14 @@ def test_schedule_circuit_in_sec_when_no_one_tells_dt(self): qc.delay(100, 0, unit="ns") qc.h(0) qc.h(1) - sc = transpile( - qc, self.backend_without_dt, scheduling_method="alap", layout_method="trivial" - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile( + qc, self.backend_without_dt, scheduling_method="alap", layout_method="trivial" + ) self.assertAlmostEqual(sc.duration, 450610 * self.dt) self.assertEqual(sc.unit, "s") self.assertEqual(sc.data[0].operation.name, "delay") @@ -128,13 +138,23 @@ def test_cannot_schedule_circuit_with_mixed_SI_and_dt_when_no_one_tells_dt(self) qc.delay(30, 0, unit="dt") qc.h(0) qc.h(1) - with self.assertRaises(QiskitError): - transpile(qc, self.backend_without_dt, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + with self.assertRaises(QiskitError): + transpile(qc, self.backend_without_dt, scheduling_method="alap") def test_transpile_single_delay_circuit(self): qc = QuantumCircuit(1) qc.delay(1234, 0) - sc = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") self.assertEqual(sc.duration, 1234) self.assertEqual(sc.data[0].operation.name, "delay") self.assertEqual(sc.data[0].operation.duration, 1234) @@ -145,7 +165,12 @@ def test_transpile_t1_circuit(self): qc.x(0) # 320 [dt] qc.delay(1000, 0, unit="ns") # 4500 [dt] qc.measure_all() - scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") self.assertEqual(scheduled.duration, 8004) def test_transpile_delay_circuit_with_backend(self): @@ -153,9 +178,14 @@ def test_transpile_delay_circuit_with_backend(self): qc.h(0) qc.delay(100, 1, unit="ns") # 450 [dt] qc.cx(0, 1) # 1760 [dt] - scheduled = transpile( - qc, backend=self.backend_with_dt, scheduling_method="alap", layout_method="trivial" - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile( + qc, backend=self.backend_with_dt, scheduling_method="alap", layout_method="trivial" + ) self.assertEqual(scheduled.duration, 1826) def test_transpile_delay_circuit_without_backend(self): @@ -187,7 +217,12 @@ def test_transpile_circuit_with_custom_instruction(self): def test_transpile_delay_circuit_with_dt_but_without_scheduling_method(self): qc = QuantumCircuit(1) qc.delay(100, 0, unit="ns") - transpiled = transpile(qc, backend=self.backend_with_dt) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled = transpile(qc, backend=self.backend_with_dt) self.assertEqual(transpiled.duration, None) # not scheduled self.assertEqual(transpiled.data[0].operation.duration, 450) # unit is converted ns -> dt @@ -212,7 +247,12 @@ def test_invalidate_schedule_circuit_if_new_instruction_is_appended(self): qc.h(0) qc.delay(500 * self.dt, 1, "s") qc.cx(0, 1) - scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="alap") # append a gate to a scheduled circuit scheduled.h(0) self.assertEqual(scheduled.duration, None) @@ -245,21 +285,31 @@ def test_unit_seconds_when_using_backend_durations(self): qc.delay(500 * self.dt, 1, "s") qc.cx(0, 1) # usual case - scheduled = transpile( - qc, backend=self.backend_with_dt, scheduling_method="alap", layout_method="trivial" - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile( + qc, backend=self.backend_with_dt, scheduling_method="alap", layout_method="trivial" + ) self.assertEqual(scheduled.duration, 1876) # update durations durations = InstructionDurations.from_backend(self.backend_with_dt) durations.update([("cx", [0, 1], 1000 * self.dt, "s")]) - scheduled = transpile( - qc, - backend=self.backend_with_dt, - scheduling_method="alap", - instruction_durations=durations, - layout_method="trivial", - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile( + qc, + backend=self.backend_with_dt, + scheduling_method="alap", + instruction_durations=durations, + layout_method="trivial", + ) self.assertEqual(scheduled.duration, 1500) def test_per_qubit_durations(self): @@ -348,13 +398,23 @@ def test_change_dt_in_transpile(self): qc.x(0) qc.measure(0, 0) # default case - scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="asap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="asap") org_duration = scheduled.duration # halve dt in sec = double duration in dt - scheduled = transpile( - qc, backend=self.backend_with_dt, scheduling_method="asap", dt=self.dt / 2 - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + scheduled = transpile( + qc, backend=self.backend_with_dt, scheduling_method="asap", dt=self.dt / 2 + ) self.assertEqual(scheduled.duration, org_duration * 2) @data("asap", "alap") @@ -366,7 +426,12 @@ def test_duration_on_same_instruction_instance(self, scheduling_method): qc = QuantumCircuit(3) qc.cz(0, 1) qc.cz(1, 2) - sc = transpile(qc, backend=self.backend_with_dt, scheduling_method=scheduling_method) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile(qc, backend=self.backend_with_dt, scheduling_method=scheduling_method) cxs = [inst.operation for inst in sc.data if inst.operation.name == "cx"] self.assertNotEqual(cxs[0].duration, cxs[1].duration) @@ -404,7 +469,12 @@ def test_can_transpile_circuits_after_assigning_parameters(self): qc.delay(idle_dur, 0, "us") qc.measure(0, 0) qc = qc.assign_parameters({idle_dur: 0.1}) - circ = transpile(qc, self.backend_with_dt) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + circ = transpile(qc, self.backend_with_dt) self.assertEqual(circ.duration, None) # not scheduled self.assertEqual(circ.data[1].operation.duration, 450) # converted in dt @@ -414,7 +484,12 @@ def test_can_transpile_and_assemble_circuits_with_assigning_parameters_inbetween qc.x(0) qc.delay(idle_dur, 0, "us") qc.measure(0, 0) - circ = transpile(qc, self.backend_with_dt) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + circ = transpile(qc, self.backend_with_dt) circ = circ.assign_parameters({idle_dur: 0.1}) with self.assertWarns(DeprecationWarning): qobj = assemble(circ, self.backend_with_dt) @@ -428,7 +503,12 @@ def test_can_transpile_circuits_with_unbounded_parameters(self): qc.delay(idle_dur, 0, "us") qc.measure(0, 0) # not assign parameter - circ = transpile(qc, self.backend_with_dt) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + circ = transpile(qc, self.backend_with_dt) self.assertEqual(circ.duration, None) # not scheduled self.assertEqual(circ.data[1].operation.unit, "dt") # converted in dt self.assertEqual( @@ -441,7 +521,12 @@ def test_fail_to_assemble_circuits_with_unbounded_parameters(self): qc.x(0) qc.delay(idle_dur, 0, "us") qc.measure(0, 0) - qc = transpile(qc, self.backend_with_dt) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + qc = transpile(qc, self.backend_with_dt) with self.assertRaises(DeprecationWarning): assemble(qc, self.backend_with_dt) @@ -453,7 +538,12 @@ def test_can_schedule_circuits_with_bounded_parameters(self, scheduling_method): qc.delay(idle_dur, 0, "us") qc.measure(0, 0) qc = qc.assign_parameters({idle_dur: 0.1}) - circ = transpile(qc, self.backend_with_dt, scheduling_method=scheduling_method) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + circ = transpile(qc, self.backend_with_dt, scheduling_method=scheduling_method) self.assertIsNotNone(circ.duration) # scheduled @data("asap", "alap") @@ -463,6 +553,12 @@ def test_fail_to_schedule_circuits_with_unbounded_parameters(self, scheduling_me qc.x(0) qc.delay(idle_dur, 0, "us") qc.measure(0, 0) - # not assign parameter - with self.assertRaises(TranspilerError): - transpile(qc, self.backend_with_dt, scheduling_method=scheduling_method) + + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + # unassigned parameter + with self.assertRaises(TranspilerError): + transpile(qc, self.backend_with_dt, scheduling_method=scheduling_method) diff --git a/test/python/compiler/test_sequencer.py b/test/python/compiler/test_sequencer.py index e32d6370367b..ae75348a5cdf 100644 --- a/test/python/compiler/test_sequencer.py +++ b/test/python/compiler/test_sequencer.py @@ -41,9 +41,19 @@ def test_transpile_and_sequence_agree_with_schedule(self): qc.h(0) qc.cx(0, 1) qc.measure_all() - sc = transpile(qc, self.backend, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile(qc, self.backend, scheduling_method="alap") actual = sequence(sc, self.backend) - expected = schedule(transpile(qc, self.backend), self.backend) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + expected = schedule(transpile(qc, self.backend), self.backend) self.assertEqual(actual, pad(expected)) def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self): @@ -52,9 +62,19 @@ def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self) qc.delay(500, 0, unit="ns") qc.h(0) qc.measure(0, 0) - sc = transpile(qc, self.backend, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile(qc, self.backend, scheduling_method="alap") actual = sequence(sc, self.backend) - expected = schedule(transpile(qc, self.backend), self.backend) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + expected = schedule(transpile(qc, self.backend), self.backend) self.assertEqual( actual.exclude(instruction_types=[pulse.Delay]), expected.exclude(instruction_types=[pulse.Delay]), @@ -65,7 +85,17 @@ def test_transpile_and_sequence_agree_with_schedule_for_circuits_without_measure qc = QuantumCircuit(2, name="bell_without_measurement") qc.h(0) qc.cx(0, 1) - sc = transpile(qc, self.backend, scheduling_method="alap") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + sc = transpile(qc, self.backend, scheduling_method="alap") actual = sequence(sc, self.backend) - expected = schedule(transpile(qc, self.backend), self.backend) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + expected = schedule(transpile(qc, self.backend), self.backend) self.assertEqual(actual, pad(expected)) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 46acff9d40d6..90dda73c0739 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1305,7 +1305,7 @@ def test_transpiled_basis_gates_calibrations(self): circ.add_calibration("h", [0], q0_x180) transpiled_circuit = transpile( - circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42 + circ, backend=GenericBackendV2(num_qubits=4, seed=42), seed_transpiler=42 ) self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) @@ -1325,7 +1325,7 @@ def test_transpile_calibrated_custom_gate_on_diff_qubit(self): with self.assertRaises(QiskitError): transpile( circ, - backend=GenericBackendV2(num_qubits=4), + backend=GenericBackendV2(num_qubits=4, seed=42), layout_method="trivial", seed_transpiler=42, optimization_level=1, @@ -1368,7 +1368,7 @@ def test_transpile_subset_of_calibrated_gates(self): transpiled_circ = transpile( circ, - backend=GenericBackendV2(num_qubits=4), + backend=GenericBackendV2(num_qubits=4, seed=42), layout_method="trivial", seed_transpiler=42, ) @@ -1390,7 +1390,7 @@ def q0_rxt(tau): transpiled_circ = transpile( circ, - backend=GenericBackendV2(num_qubits=4), + backend=GenericBackendV2(num_qubits=4, seed=42), layout_method="trivial", seed_transpiler=42, ) @@ -1554,11 +1554,12 @@ def test_scheduling_timing_constraints(self): [0, 0], ) with self.assertRaisesRegex(TranspilerError, error_msgs[duration]): - _ = transpile( - qc, - backend=backend, - timing_constraints=timing_constraints, - ) + with self.assertWarns(DeprecationWarning): + _ = transpile( + qc, + backend=backend, + timing_constraints=timing_constraints, + ) def test_scheduling_instruction_constraints(self): """Test that scheduling-related loose transpile constraints @@ -1566,7 +1567,7 @@ def test_scheduling_instruction_constraints(self): with self.assertWarns(DeprecationWarning): backend_v1 = Fake27QPulseV1() - backend_v2 = BackendV2Converter(backend_v1) + backend_v2 = BackendV2Converter(backend_v1) qc = QuantumCircuit(2) qc.h(0) qc.delay(500, 1, "dt") @@ -1575,16 +1576,27 @@ def test_scheduling_instruction_constraints(self): durations = InstructionDurations.from_backend(backend_v1) durations.update([("cx", [0, 1], 1000, "dt")]) - for backend in [backend_v1, backend_v2]: - with self.subTest(backend=backend): - scheduled = transpile( - qc, - backend=backend, - scheduling_method="alap", - instruction_durations=durations, - layout_method="trivial", - ) - self.assertEqual(scheduled.duration, 1500) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will stop supporting inputs of type `BackendV1` ", + ): + scheduled = transpile( + qc, + backend=backend_v1, + scheduling_method="alap", + instruction_durations=durations, + layout_method="trivial", + ) + self.assertEqual(scheduled.duration, 1500) + + scheduled = transpile( + qc, + backend=backend_v2, + scheduling_method="alap", + instruction_durations=durations, + layout_method="trivial", + ) + self.assertEqual(scheduled.duration, 1500) def test_scheduling_dt_constraints(self): """Test that scheduling-related loose transpile constraints @@ -1592,20 +1604,26 @@ def test_scheduling_dt_constraints(self): with self.assertWarns(DeprecationWarning): backend_v1 = Fake27QPulseV1() - backend_v2 = BackendV2Converter(backend_v1) + backend_v2 = BackendV2Converter(backend_v1) qc = QuantumCircuit(1, 1) qc.x(0) qc.measure(0, 0) original_dt = 2.2222222222222221e-10 original_duration = 3504 - for backend in [backend_v1, backend_v2]: - with self.subTest(backend=backend): - # halve dt in sec = double duration in dt - scheduled = transpile( - qc, backend=backend, scheduling_method="asap", dt=original_dt / 2 - ) - self.assertEqual(scheduled.duration, original_duration * 2) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will stop supporting inputs of type `BackendV1` ", + ): + # halve dt in sec = double duration in dt + scheduled = transpile( + qc, backend=backend_v1, scheduling_method="asap", dt=original_dt / 2 + ) + self.assertEqual(scheduled.duration, original_duration * 2) + + # halve dt in sec = double duration in dt + scheduled = transpile(qc, backend=backend_v2, scheduling_method="asap", dt=original_dt / 2) + self.assertEqual(scheduled.duration, original_duration * 2) def test_backend_props_constraints(self): """Test that loose transpile constraints @@ -1613,7 +1631,7 @@ def test_backend_props_constraints(self): with self.assertWarns(DeprecationWarning): backend_v1 = Fake20QV1() - backend_v2 = BackendV2Converter(backend_v1) + backend_v2 = BackendV2Converter(backend_v1) qr1 = QuantumRegister(3, "qr1") qr2 = QuantumRegister(2, "qr2") qc = QuantumCircuit(qr1, qr2) @@ -1625,7 +1643,8 @@ def test_backend_props_constraints(self): # generate a fake backend with same number of qubits # but different backend properties fake_backend = GenericBackendV2(num_qubits=20, seed=42) - custom_backend_properties = target_to_backend_properties(fake_backend.target) + with self.assertWarns(DeprecationWarning): + custom_backend_properties = target_to_backend_properties(fake_backend.target) # expected layout for custom_backend_properties # (different from expected layout for Fake20QV1) @@ -1652,17 +1671,28 @@ def test_backend_props_constraints(self): 17: Qubit(QuantumRegister(15, "ancilla"), 14), } - for backend in [backend_v1, backend_v2]: - with self.subTest(backend=backend): - result = transpile( - qc, - backend=backend, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will stop supporting inputs of type `BackendV1` ", + ): + result = transpile( + qc, + backend=backend_v1, + backend_properties=custom_backend_properties, + optimization_level=2, + seed_transpiler=42, + ) + + self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) + result = transpile( + qc, + backend=backend_v2, + backend_properties=custom_backend_properties, + optimization_level=2, + seed_transpiler=42, + ) - self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) + self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) @data(1, 2, 3) def test_no_infinite_loop(self, optimization_level): @@ -2101,9 +2131,9 @@ def test_transpile_annotated_ops_with_backend_v1(self, opt_level): qc.append(AnnotatedOperation(HGate(), PowerModifier(3)), [2]) with self.assertWarns(DeprecationWarning): backend = Fake20QV1() - transpiled = transpile( - qc, optimization_level=opt_level, backend=backend, seed_transpiler=42 - ) + transpiled = transpile( + qc, optimization_level=opt_level, backend=backend, seed_transpiler=42 + ) self.assertLessEqual(set(transpiled.count_ops().keys()), {"u1", "u2", "u3", "cx"}) @combine(opt_level=[0, 1, 2, 3]) @@ -2449,12 +2479,12 @@ def test_qasm3_output_v1(self, optimization_level): with self.assertWarns(DeprecationWarning): backend = Fake20QV1() - transpiled = transpile( - self._regular_circuit(), - backend=backend, - optimization_level=optimization_level, - seed_transpiler=2022_10_17, - ) + transpiled = transpile( + self._regular_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2022_10_17, + ) # TODO: There's not a huge amount we can sensibly test for the output here until we can # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump # itself doesn't throw an error, though. diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index ad6b4c6abfdb..12a932b83819 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -33,8 +33,20 @@ from qiskit.quantum_info import SparsePauliOp from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.utils import optionals - -BACKENDS = [BasicSimulator(), Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())] +from ..legacy_cmaps import LAGOS_CMAP + +BACKENDS_V1 = [Fake7QPulseV1()] +BACKENDS_V2 = [ + BasicSimulator(), + BackendV2Converter(Fake7QPulseV1()), + GenericBackendV2( + num_qubits=7, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=LAGOS_CMAP, + seed=42, + ), +] +BACKENDS = BACKENDS_V1 + BACKENDS_V2 @ddt @@ -73,7 +85,7 @@ def setUp(self): [1, 2, 3, 4, 5, 6], ) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_estimator_run(self, backend, abelian_grouping): """Test Estimator.run()""" psi1, psi2 = self.psi @@ -120,7 +132,59 @@ def test_estimator_run(self, backend, abelian_grouping): np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_estimator_run_v1(self, backend, abelian_grouping): + """Test Estimator.run()""" + psi1, psi2 = self.psi + hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian + theta1, theta2, theta3 = self.theta + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1, psi2 = pm.run([psi1, psi2]) + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + # Specify the circuit and observable by indices. + # calculate [ ] + ham1 = hamiltonian1.apply_layout(psi1.layout) + job = estimator.run([(psi1, ham1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol) + + # Objects can be passed instead of indices. + # Note that passing objects has an overhead + # since the corresponding indices need to be searched. + # User can append a circuit and observable. + # calculate [ ] + ham1 = hamiltonian1.apply_layout(psi2.layout) + result2 = estimator.run([(psi2, ham1, theta2)]).result() + np.testing.assert_allclose(result2[0].data.evs, [2.97797666], rtol=self._rtol) + + # calculate [ , ] + ham2 = hamiltonian2.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + result3 = estimator.run([(psi1, [ham2, ham3], theta1)]).result() + np.testing.assert_allclose(result3[0].data.evs, [-0.551653, 0.07535239], rtol=self._rtol) + + # calculate [ [, + # ], + # [] ] + ham1 = hamiltonian1.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + ham2 = hamiltonian2.apply_layout(psi2.layout) + result4 = estimator.run( + [ + (psi1, [ham1, ham3], [theta1, theta3]), + (psi2, ham2, theta2), + ] + ).result() + np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) + np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_estimator_with_pub(self, backend, abelian_grouping): """Test estimator with explicit EstimatorPubs.""" psi1, psi2 = self.psi @@ -146,7 +210,38 @@ def test_estimator_with_pub(self, backend, abelian_grouping): np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_estimator_with_pub_v1(self, backend, abelian_grouping): + """Test estimator with explicit EstimatorPubs.""" + psi1, psi2 = self.psi + hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian + theta1, theta2, theta3 = self.theta + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1, psi2 = pm.run([psi1, psi2]) + + ham1 = hamiltonian1.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + obs1 = ObservablesArray.coerce([ham1, ham3]) + bind1 = BindingsArray.coerce({tuple(psi1.parameters): [theta1, theta3]}) + pub1 = EstimatorPub(psi1, obs1, bind1) + + ham2 = hamiltonian2.apply_layout(psi2.layout) + obs2 = ObservablesArray.coerce(ham2) + bind2 = BindingsArray.coerce({tuple(psi2.parameters): theta2}) + pub2 = EstimatorPub(psi2, obs2, bind2) + + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + result4 = estimator.run([pub1, pub2]).result() + np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) + np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_estimator_run_no_params(self, backend, abelian_grouping): """test for estimator without parameters""" circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) @@ -158,7 +253,24 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): result = est.run([(circuit, observable)]).result() np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_estimator_run_no_params_v1(self, backend, abelian_grouping): + """test for estimator without parameters""" + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + circuit = pm.run(circuit) + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + observable = self.observable.apply_layout(circuit.layout) + result = est.run([(circuit, observable)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_run_single_circuit_observable(self, backend, abelian_grouping): """Test for single circuit and single observable case.""" est = BackendEstimatorV2(backend=backend, options=self._options) @@ -216,7 +328,70 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) self.assertEqual(result[0].metadata["target_precision"], self._precision) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_run_single_circuit_observable_v1(self, backend, abelian_grouping): + """Test for single circuit and single observable case.""" + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + with self.subTest("No parameter"): + qc = QuantumCircuit(1) + qc.x(0) + qc = pm.run(qc) + op = SparsePauliOp("Z") + op = op.apply_layout(qc.layout) + param_vals = [None, [], [[]], np.array([]), np.array([[]]), [np.array([])]] + target = [-1] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + with self.subTest("One parameter"): + param = Parameter("x") + qc = QuantumCircuit(1) + qc.ry(param, 0) + qc = pm.run(qc) + op = SparsePauliOp("Z") + op = op.apply_layout(qc.layout) + param_vals = [ + [np.pi], + np.array([np.pi]), + ] + target = [-1] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + with self.subTest("More than one parameter"): + qc = self.psi[0] + qc = pm.run(qc) + op = self.hamiltonian[0] + op = op.apply_layout(qc.layout) + param_vals = [ + self.theta[0], + [self.theta[0]], + np.array(self.theta[0]), + np.array([self.theta[0]]), + [np.array(self.theta[0])], + ] + target = [1.5555572817900956] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_run_1qubit(self, backend, abelian_grouping): """Test for 1-qubit cases""" qc = QuantumCircuit(1) @@ -246,7 +421,42 @@ def test_run_1qubit(self, backend, abelian_grouping): result = est.run([(qc2, op_4)]).result() np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_run_1qubit_v1(self, backend, abelian_grouping): + """Test for 1-qubit cases""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(1) + qc2.x(0) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("Z", 1)]) + + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + op_1 = op.apply_layout(qc.layout) + result = est.run([(qc, op_1)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_2 = op2.apply_layout(qc.layout) + result = est.run([(qc, op_2)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_3 = op.apply_layout(qc2.layout) + result = est.run([(qc2, op_3)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_4 = op2.apply_layout(qc2.layout) + result = est.run([(qc2, op_4)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_run_2qubits(self, backend, abelian_grouping): """Test for 2-qubit cases (to check endian)""" qc = QuantumCircuit(2) @@ -285,6 +495,50 @@ def test_run_2qubits(self, backend, abelian_grouping): result = est.run([(qc2, op_6)]).result() np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_run_2qubits_v1(self, backend, abelian_grouping): + """Test for 2-qubit cases (to check endian)""" + qc = QuantumCircuit(2) + qc2 = QuantumCircuit(2) + qc2.x(0) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + + op = SparsePauliOp.from_list([("II", 1)]) + op2 = SparsePauliOp.from_list([("ZI", 1)]) + op3 = SparsePauliOp.from_list([("IZ", 1)]) + + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + op_1 = op.apply_layout(qc.layout) + result = est.run([(qc, op_1)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_2 = op.apply_layout(qc2.layout) + result = est.run([(qc2, op_2)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_3 = op2.apply_layout(qc.layout) + result = est.run([(qc, op_3)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_4 = op2.apply_layout(qc2.layout) + result = est.run([(qc2, op_4)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_5 = op3.apply_layout(qc.layout) + result = est.run([(qc, op_5)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_6 = op3.apply_layout(qc2.layout) + result = est.run([(qc2, op_6)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) + @combine(backend=BACKENDS, abelian_grouping=[True, False]) def test_run_errors(self, backend, abelian_grouping): """Test for errors""" @@ -322,7 +576,7 @@ def test_run_errors(self, backend, abelian_grouping): with self.assertRaisesRegex(ValueError, "An invalid Estimator pub-like was given"): _ = est.run((qc, op)).result() - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_run_numpy_params(self, backend, abelian_grouping): """Test for numpy array as parameter values""" qc = RealAmplitudes(num_qubits=2, reps=2) @@ -350,7 +604,40 @@ def test_run_numpy_params(self, backend, abelian_grouping): self.assertEqual(result[0].data.evs.shape, (k,)) np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_run_numpy_params_v1(self, backend, abelian_grouping): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) + op = op.apply_layout(qc.layout) + k = 5 + params_array = self._rng.random((k, qc.num_parameters)) + params_list = params_array.tolist() + params_list_array = list(params_array) + statevector_estimator = StatevectorEstimator(seed=123) + target = statevector_estimator.run([(qc, op, params_list)]).result() + + backend_estimator = BackendEstimatorV2(backend=backend, options=self._options) + backend_estimator.options.abelian_grouping = abelian_grouping + + with self.subTest("ndarrary"): + result = backend_estimator.run([(qc, op, params_array)]).result() + self.assertEqual(result[0].data.evs.shape, (k,)) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + with self.subTest("list of ndarray"): + result = backend_estimator.run([(qc, op, params_list_array)]).result() + self.assertEqual(result[0].data.evs.shape, (k,)) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_precision(self, backend, abelian_grouping): """Test for precision""" estimator = BackendEstimatorV2(backend=backend, options=self._options) @@ -372,7 +659,34 @@ def test_precision(self, backend, abelian_grouping): result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_precision_v1(self, backend, abelian_grouping): + """Test for precision""" + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1 = pm.run(self.psi[0]) + hamiltonian1 = self.hamiltonian[0].apply_layout(psi1.layout) + theta1 = self.theta[0] + job = estimator.run([(psi1, hamiltonian1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) + # The result of the second run is the same + job = estimator.run([(psi1, hamiltonian1, [theta1]), (psi1, hamiltonian1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) + np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol) + # apply smaller precision value + job = estimator.run([(psi1, hamiltonian1, [theta1])], precision=self._precision * 0.5) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol) + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_diff_precision(self, backend, abelian_grouping): """Test for running different precisions at once""" estimator = BackendEstimatorV2(backend=backend, options=self._options) @@ -388,6 +702,27 @@ def test_diff_precision(self, backend, abelian_grouping): np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_diff_precision_v1(self, backend, abelian_grouping): + """Test for running different precisions at once""" + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will stop supporting " + "inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1 = pm.run(self.psi[0]) + hamiltonian1 = self.hamiltonian[0].apply_layout(psi1.layout) + theta1 = self.theta[0] + job = estimator.run( + [(psi1, hamiltonian1, [theta1]), (psi1, hamiltonian1, [theta1], self._precision * 0.8)] + ) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) + np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol) + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") @combine(abelian_grouping=[True, False]) def test_aer(self, abelian_grouping): diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index f0fdb4f07f8c..27e438d30874 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -34,6 +34,12 @@ BACKENDS = [Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())] +BACKENDS_V1 = [Fake7QPulseV1()] +BACKENDS_V2 = [ + BackendV2Converter(Fake7QPulseV1()), +] +BACKENDS = BACKENDS_V1 + BACKENDS_V2 + class CallbackPass(DummyAP): """A dummy analysis pass that calls a callback when executed""" @@ -236,8 +242,37 @@ def test_run_errors(self, backend): with self.assertRaises(ValueError): sampler.run([qc2], [[1e2]]).result() - @combine(backend=BACKENDS) - def test_run_empty_parameter(self, backend): + @combine(backend=BACKENDS_V1) + def test_run_empty_parameter_v1(self, backend): + """Test for empty parameter""" + n = 5 + qc = QuantumCircuit(n, n - 1) + qc.measure(range(n - 1), range(n - 1)) + with self.assertWarns(DeprecationWarning): + sampler = BackendSampler(backend=backend) + with self.subTest("one circuit"): + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + result = sampler.run([qc], shots=1000).result() + self.assertEqual(len(result.quasi_dists), 1) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictAlmostEqual(quasi_dist, {0: 1.0}, delta=0.1) + self.assertEqual(len(result.metadata), 1) + + with self.subTest("two circuits"): + result = sampler.run([qc, qc], shots=1000).result() + self.assertEqual(len(result.quasi_dists), 2) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictAlmostEqual(quasi_dist, {0: 1.0}, delta=0.1) + self.assertEqual(len(result.metadata), 2) + + @combine(backend=BACKENDS_V2) + def test_run_empty_parameter_v2(self, backend): """Test for empty parameter""" n = 5 qc = QuantumCircuit(n, n - 1) diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 6e899dca58f9..372ae3a6715c 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -34,8 +34,20 @@ from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.fake_provider import Fake7QPulseV1, GenericBackendV2 from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - -BACKENDS = [BasicSimulator(), Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())] +from ..legacy_cmaps import LAGOS_CMAP + +BACKENDS_V1 = [Fake7QPulseV1()] +BACKENDS_V2 = [ + BasicSimulator(), + BackendV2Converter(Fake7QPulseV1()), + GenericBackendV2( + num_qubits=7, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=LAGOS_CMAP, + seed=42, + ), +] +BACKENDS = BACKENDS_V1 + BACKENDS_V2 @ddt @@ -85,7 +97,63 @@ def _assert_allclose(self, bitarray: BitArray, target: NDArray | BitArray, rtol= tgt = np.array([target_counts.get(i, 0) for i in range(max_key + 1)]) np.testing.assert_allclose(ary, tgt, rtol=rtol, atol=atol, err_msg=f"index: {idx}") - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_sampler_run_v1(self, backend): + """Test run().""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + with self.subTest("single"): + bell, _, target = self._cases[1] + bell = pm.run(bell) + sampler = BackendSamplerV2(backend=backend, options=self._options) + job = sampler.run([bell], shots=self._shots) + result = job.result() + self.assertIsInstance(result, PrimitiveResult) + self.assertIsInstance(result.metadata, dict) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[0].data, DataBin) + self.assertIsInstance(result[0].data.meas, BitArray) + self._assert_allclose(result[0].data.meas, np.array(target)) + + with self.subTest("single with param"): + pqc, param_vals, target = self._cases[2] + sampler = BackendSamplerV2(backend=backend, options=self._options) + pqc = pm.run(pqc) + params = (param.name for param in pqc.parameters) + job = sampler.run([(pqc, {params: param_vals})], shots=self._shots) + result = job.result() + self.assertIsInstance(result, PrimitiveResult) + self.assertIsInstance(result.metadata, dict) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[0].data, DataBin) + self.assertIsInstance(result[0].data.meas, BitArray) + self._assert_allclose(result[0].data.meas, np.array(target)) + + with self.subTest("multiple"): + pqc, param_vals, target = self._cases[2] + sampler = BackendSamplerV2(backend=backend, options=self._options) + pqc = pm.run(pqc) + params = (param.name for param in pqc.parameters) + job = sampler.run( + [(pqc, {params: [param_vals, param_vals, param_vals]})], shots=self._shots + ) + result = job.result() + self.assertIsInstance(result, PrimitiveResult) + self.assertIsInstance(result.metadata, dict) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[0].data, DataBin) + self.assertIsInstance(result[0].data.meas, BitArray) + self._assert_allclose(result[0].data.meas, np.array([target, target, target])) + + @combine(backend=BACKENDS_V2) def test_sampler_run(self, backend): """Test run().""" pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -136,7 +204,25 @@ def test_sampler_run(self, backend): self.assertIsInstance(result[0].data.meas, BitArray) self._assert_allclose(result[0].data.meas, np.array([target, target, target])) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_sampler_run_multiple_times_v1(self, backend): + """Test run() returns the same results if the same input is given.""" + bell, _, _ = self._cases[1] + sampler = BackendSamplerV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + bell = pm.run(bell) + result1 = sampler.run([bell], shots=self._shots).result() + meas1 = result1[0].data.meas + result2 = sampler.run([bell], shots=self._shots).result() + meas2 = result2[0].data.meas + self._assert_allclose(meas1, meas2, rtol=0) + + @combine(backend=BACKENDS_V2) def test_sampler_run_multiple_times(self, backend): """Test run() returns the same results if the same input is given.""" bell, _, _ = self._cases[1] @@ -149,7 +235,25 @@ def test_sampler_run_multiple_times(self, backend): meas2 = result2[0].data.meas self._assert_allclose(meas1, meas2, rtol=0) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_sample_run_multiple_circuits_v1(self, backend): + """Test run() with multiple circuits.""" + bell, _, target = self._cases[1] + sampler = BackendSamplerV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + bell = pm.run(bell) + result = sampler.run([bell, bell, bell], shots=self._shots).result() + self.assertEqual(len(result), 3) + self._assert_allclose(result[0].data.meas, np.array(target)) + self._assert_allclose(result[1].data.meas, np.array(target)) + self._assert_allclose(result[2].data.meas, np.array(target)) + + @combine(backend=BACKENDS_V2) def test_sample_run_multiple_circuits(self, backend): """Test run() with multiple circuits.""" bell, _, target = self._cases[1] @@ -162,13 +266,14 @@ def test_sample_run_multiple_circuits(self, backend): self._assert_allclose(result[1].data.meas, np.array(target)) self._assert_allclose(result[2].data.meas, np.array(target)) - @combine(backend=BACKENDS) - def test_sampler_run_with_parameterized_circuits(self, backend): + @combine(backend=BACKENDS_V1) + def test_sampler_run_with_parameterized_circuits_v1(self, backend): """Test run() with parameterized circuits.""" pqc1, param1, target1 = self._cases[4] pqc2, param2, target2 = self._cases[5] pqc3, param3, target3 = self._cases[6] - pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + with self.assertWarns(DeprecationWarning): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) pqc1, pqc2, pqc3 = pm.run([pqc1, pqc2, pqc3]) sampler = BackendSamplerV2(backend=backend, options=self._options) @@ -180,7 +285,7 @@ def test_sampler_run_with_parameterized_circuits(self, backend): self._assert_allclose(result[1].data.meas, np.array(target2)) self._assert_allclose(result[2].data.meas, np.array(target3)) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V2) def test_run_1qubit(self, backend): """test for 1-qubit cases""" qc = QuantumCircuit(1) @@ -197,7 +302,29 @@ def test_run_1qubit(self, backend): for i in range(2): self._assert_allclose(result[i].data.meas, np.array({i: self._shots})) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_1qubit_v1(self, backend): + """test for 1-qubit cases""" + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([qc, qc2], shots=self._shots).result() + self.assertEqual(len(result), 2) + for i in range(2): + self._assert_allclose(result[i].data.meas, np.array({i: self._shots})) + + @combine(backend=BACKENDS_V2) def test_run_2qubit(self, backend): """test for 2-qubit cases""" qc0 = QuantumCircuit(2) @@ -220,7 +347,35 @@ def test_run_2qubit(self, backend): for i in range(4): self._assert_allclose(result[i].data.meas, np.array({i: self._shots})) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_2qubit_v1(self, backend): + """test for 2-qubit cases""" + qc0 = QuantumCircuit(2) + qc0.measure_all() + qc1 = QuantumCircuit(2) + qc1.x(0) + qc1.measure_all() + qc2 = QuantumCircuit(2) + qc2.x(1) + qc2.measure_all() + qc3 = QuantumCircuit(2) + qc3.x([0, 1]) + qc3.measure_all() + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc0, qc1, qc2, qc3 = pm.run([qc0, qc1, qc2, qc3]) + + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([qc0, qc1, qc2, qc3], shots=self._shots).result() + self.assertEqual(len(result), 4) + for i in range(4): + self._assert_allclose(result[i].data.meas, np.array({i: self._shots})) + + @combine(backend=BACKENDS_V2) def test_run_single_circuit(self, backend): """Test for single circuit case.""" sampler = BackendSamplerV2(backend=backend, options=self._options) @@ -278,7 +433,70 @@ def test_run_single_circuit(self, backend): self.assertEqual(len(result), 1) self._assert_allclose(result[0].data.meas, target) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_single_circuit_v1(self, backend): + """Test for single circuit case.""" + sampler = BackendSamplerV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + with self.subTest("No parameter"): + circuit, _, target = self._cases[1] + circuit = pm.run(circuit) + param_target = [ + (None, np.array(target)), + ({}, np.array(target)), + ] + for param, target in param_target: + with self.subTest(f"{circuit.name} w/ {param}"): + result = sampler.run([(circuit, param)], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.meas, target) + + with self.subTest("One parameter"): + circuit = QuantumCircuit(1, 1, name="X gate") + param = Parameter("x") + circuit.ry(param, 0) + circuit.measure(0, 0) + circuit = pm.run(circuit) + param_target = [ + ({"x": np.pi}, np.array({1: self._shots})), + ({param: np.pi}, np.array({1: self._shots})), + ({"x": np.array(np.pi)}, np.array({1: self._shots})), + ({param: np.array(np.pi)}, np.array({1: self._shots})), + ({"x": [np.pi]}, np.array({1: self._shots})), + ({param: [np.pi]}, np.array({1: self._shots})), + ({"x": np.array([np.pi])}, np.array({1: self._shots})), + ({param: np.array([np.pi])}, np.array({1: self._shots})), + ] + for param, target in param_target: + with self.subTest(f"{circuit.name} w/ {param}"): + result = sampler.run([(circuit, param)], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.c, target) + + with self.subTest("More than one parameter"): + circuit, param, target = self._cases[3] + circuit = pm.run(circuit) + param_target = [ + (param, np.array(target)), + (tuple(param), np.array(target)), + (np.array(param), np.array(target)), + ((param,), np.array([target])), + ([param], np.array([target])), + (np.array([param]), np.array([target])), + ] + for param, target in param_target: + with self.subTest(f"{circuit.name} w/ {param}"): + result = sampler.run([(circuit, param)], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.meas, target) + + @combine(backend=BACKENDS_V2) def test_run_reverse_meas_order(self, backend): """test for sampler with reverse measurement order""" x = Parameter("x") @@ -305,7 +523,39 @@ def test_run_reverse_meas_order(self, backend): # qc({x: pi/2, y: 0}) self._assert_allclose(result[1].data.c, np.array({1: self._shots / 2, 5: self._shots / 2})) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_reverse_meas_order_v1(self, backend): + """test for sampler with reverse measurement order""" + x = Parameter("x") + y = Parameter("y") + + qc = QuantumCircuit(3, 3) + qc.rx(x, 0) + qc.rx(y, 1) + qc.x(2) + qc.measure(0, 2) + qc.measure(1, 1) + qc.measure(2, 0) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + + sampler = BackendSamplerV2(backend=backend) + sampler.options.seed_simulator = self._seed + result = sampler.run([(qc, [0, 0]), (qc, [np.pi / 2, 0])], shots=self._shots).result() + self.assertEqual(len(result), 2) + + # qc({x: 0, y: 0}) + self._assert_allclose(result[0].data.c, np.array({1: self._shots})) + + # qc({x: pi/2, y: 0}) + self._assert_allclose(result[1].data.c, np.array({1: self._shots / 2, 5: self._shots / 2})) + + @combine(backend=BACKENDS_V2) def test_run_errors(self, backend): """Test for errors with run method""" qc1 = QuantumCircuit(1) @@ -357,7 +607,64 @@ def test_run_errors(self, backend): with self.assertRaisesRegex(ValueError, "Note that if you want to run a single pub,"): _ = sampler.run((qc2, [0, 1])).result() - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_errors_v1(self, backend): + """Test for errors with run method""" + qc1 = QuantumCircuit(1) + qc1.measure_all() + qc2 = RealAmplitudes(num_qubits=1, reps=1) + qc2.measure_all() + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc1, qc2 = pm.run([qc1, qc2]) + + sampler = BackendSamplerV2(backend=backend) + with self.subTest("set parameter values to a non-parameterized circuit"): + with self.assertRaises(ValueError): + _ = sampler.run([(qc1, [1e2])]).result() + with self.subTest("missing all parameter values for a parameterized circuit"): + with self.assertRaises(ValueError): + _ = sampler.run([qc2]).result() + with self.assertRaises(ValueError): + _ = sampler.run([(qc2, [])]).result() + with self.assertRaises(ValueError): + _ = sampler.run([(qc2, None)]).result() + with self.subTest("missing some parameter values for a parameterized circuit"): + with self.assertRaises(ValueError): + _ = sampler.run([(qc2, [1e2])]).result() + with self.subTest("too many parameter values for a parameterized circuit"): + with self.assertRaises(ValueError): + _ = sampler.run([(qc2, [1e2] * 100)]).result() + with self.subTest("negative shots, run arg"): + with self.assertRaises(ValueError): + _ = sampler.run([qc1], shots=-1).result() + with self.subTest("negative shots, pub-like"): + with self.assertRaises(ValueError): + _ = sampler.run([(qc1, None, -1)]).result() + with self.subTest("negative shots, pub"): + with self.assertRaises(ValueError): + _ = sampler.run([SamplerPub(qc1, shots=-1)]).result() + with self.subTest("zero shots, run arg"): + with self.assertRaises(ValueError): + _ = sampler.run([qc1], shots=0).result() + with self.subTest("zero shots, pub-like"): + with self.assertRaises(ValueError): + _ = sampler.run([(qc1, None, 0)]).result() + with self.subTest("zero shots, pub"): + with self.assertRaises(ValueError): + _ = sampler.run([SamplerPub(qc1, shots=0)]).result() + with self.subTest("missing []"): + with self.assertRaisesRegex(ValueError, "An invalid Sampler pub-like was given"): + _ = sampler.run(qc1).result() + with self.subTest("missing [] for pqc"): + with self.assertRaisesRegex(ValueError, "Note that if you want to run a single pub,"): + _ = sampler.run((qc2, [0, 1])).result() + + @combine(backend=BACKENDS_V2) def test_run_empty_parameter(self, backend): """Test for empty parameter""" n = 5 @@ -377,17 +684,42 @@ def test_run_empty_parameter(self, backend): for i in range(2): self._assert_allclose(result[i].data.c, np.array({0: self._shots})) - @combine(backend=BACKENDS) - def test_run_numpy_params(self, backend): - """Test for numpy array as parameter values""" - qc = RealAmplitudes(num_qubits=2, reps=2) - qc.measure_all() - pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + @combine(backend=BACKENDS_V1) + def test_run_empty_parameter_v1(self, backend): + """Test for empty parameter""" + n = 5 + qc = QuantumCircuit(n, n - 1) + qc.measure(range(n - 1), range(n - 1)) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) qc = pm.run(qc) - k = 5 - params_array = np.linspace(0, 1, k * qc.num_parameters).reshape((k, qc.num_parameters)) - params_list = params_array.tolist() - sampler = StatevectorSampler(seed=self._seed) + sampler = BackendSamplerV2(backend=backend, options=self._options) + with self.subTest("one circuit"): + result = sampler.run([qc], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.c, np.array({0: self._shots})) + + with self.subTest("two circuits"): + result = sampler.run([qc, qc], shots=self._shots).result() + self.assertEqual(len(result), 2) + for i in range(2): + self._assert_allclose(result[i].data.c, np.array({0: self._shots})) + + @combine(backend=BACKENDS_V2) + def test_run_numpy_params(self, backend): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + qc.measure_all() + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + k = 5 + params_array = np.linspace(0, 1, k * qc.num_parameters).reshape((k, qc.num_parameters)) + params_list = params_array.tolist() + sampler = StatevectorSampler(seed=self._seed) target = sampler.run([(qc, params_list)], shots=self._shots).result() with self.subTest("ndarray"): @@ -407,7 +739,42 @@ def test_run_numpy_params(self, backend): result[i].data.meas, np.array(target[0].data.meas.get_int_counts(i)) ) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_numpy_params_v1(self, backend): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + qc.measure_all() + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + k = 5 + params_array = np.linspace(0, 1, k * qc.num_parameters).reshape((k, qc.num_parameters)) + params_list = params_array.tolist() + sampler = StatevectorSampler(seed=self._seed) + target = sampler.run([(qc, params_list)], shots=self._shots).result() + + with self.subTest("ndarray"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([(qc, params_array)], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.meas, target[0].data.meas) + + with self.subTest("split a list"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run( + [(qc, params) for params in params_list], shots=self._shots + ).result() + self.assertEqual(len(result), k) + for i in range(k): + self._assert_allclose( + result[i].data.meas, np.array(target[0].data.meas.get_int_counts(i)) + ) + + @combine(backend=BACKENDS_V2) def test_run_with_shots_option(self, backend): """test with shots option.""" bell, _, _ = self._cases[1] @@ -471,7 +838,76 @@ def test_run_with_shots_option(self, backend): self.assertEqual(result[1].data.meas.num_shots, shots2) self.assertEqual(sum(result[1].data.meas.get_counts().values()), shots2) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_with_shots_option_v1(self, backend): + """test with shots option.""" + bell, _, _ = self._cases[1] + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + bell = pm.run(bell) + shots = 100 + + with self.subTest("run arg"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([bell], shots=shots).result() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].data.meas.num_shots, shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), shots) + + with self.subTest("default shots"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + default_shots = sampler.options.default_shots + result = sampler.run([bell]).result() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].data.meas.num_shots, default_shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), default_shots) + + with self.subTest("setting default shots"): + default_shots = 100 + sampler = BackendSamplerV2(backend=backend, options=self._options) + sampler.options.default_shots = default_shots + self.assertEqual(sampler.options.default_shots, default_shots) + result = sampler.run([bell]).result() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].data.meas.num_shots, default_shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), default_shots) + + with self.subTest("pub-like"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([(bell, None, shots)]).result() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].data.meas.num_shots, shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), shots) + + with self.subTest("pub"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([SamplerPub(bell, shots=shots)]).result() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].data.meas.num_shots, shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), shots) + + with self.subTest("multiple pubs"): + sampler = BackendSamplerV2(backend=backend, options=self._options) + shots1 = 100 + shots2 = 200 + result = sampler.run( + [ + SamplerPub(bell, shots=shots1), + SamplerPub(bell, shots=shots2), + ], + shots=self._shots, + ).result() + self.assertEqual(len(result), 2) + self.assertEqual(result[0].data.meas.num_shots, shots1) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), shots1) + self.assertEqual(result[1].data.meas.num_shots, shots2) + self.assertEqual(sum(result[1].data.meas.get_counts().values()), shots2) + + @combine(backend=BACKENDS_V2) def test_run_shots_result_size(self, backend): """test with shots option to validate the result size""" n = 7 # should be less than or equal to the number of qubits of backend @@ -486,7 +922,27 @@ def test_run_shots_result_size(self, backend): self.assertLessEqual(result[0].data.meas.num_shots, self._shots) self.assertEqual(sum(result[0].data.meas.get_counts().values()), self._shots) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_run_shots_result_size_v1(self, backend): + """test with shots option to validate the result size""" + n = 7 # should be less than or equal to the number of qubits of backend + qc = QuantumCircuit(n) + qc.h(range(n)) + qc.measure_all() + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([qc], shots=self._shots).result() + self.assertEqual(len(result), 1) + self.assertLessEqual(result[0].data.meas.num_shots, self._shots) + self.assertEqual(sum(result[0].data.meas.get_counts().values()), self._shots) + + @combine(backend=BACKENDS_V2) def test_primitive_job_status_done(self, backend): """test primitive job's status""" bell, _, _ = self._cases[1] @@ -497,7 +953,23 @@ def test_primitive_job_status_done(self, backend): _ = job.result() self.assertEqual(job.status(), JobStatus.DONE) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_primitive_job_status_done_v1(self, backend): + """test primitive job's status""" + bell, _, _ = self._cases[1] + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + bell = pm.run(bell) + sampler = BackendSamplerV2(backend=backend, options=self._options) + job = sampler.run([bell], shots=self._shots) + _ = job.result() + self.assertEqual(job.status(), JobStatus.DONE) + + @combine(backend=BACKENDS_V2) def test_circuit_with_unitary(self, backend): """Test for circuit with unitary gate.""" pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -528,7 +1000,43 @@ def test_circuit_with_unitary(self, backend): self.assertEqual(len(result), 1) self._assert_allclose(result[0].data.meas, np.array({1: self._shots})) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_circuit_with_unitary_v1(self, backend): + """Test for circuit with unitary gate.""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + with self.subTest("identity"): + gate = UnitaryGate(np.eye(2)) + + circuit = QuantumCircuit(1) + circuit.append(gate, [0]) + circuit.measure_all() + circuit = pm.run(circuit) + + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([circuit], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) + + with self.subTest("X"): + gate = UnitaryGate([[0, 1], [1, 0]]) + + circuit = QuantumCircuit(1) + circuit.append(gate, [0]) + circuit.measure_all() + circuit = pm.run(circuit) + + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([circuit], shots=self._shots).result() + self.assertEqual(len(result), 1) + self._assert_allclose(result[0].data.meas, np.array({1: self._shots})) + + @combine(backend=BACKENDS_V2) def test_circuit_with_multiple_cregs(self, backend): """Test for circuit with multiple classical registers.""" pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -608,7 +1116,92 @@ def test_circuit_with_multiple_cregs(self, backend): self.assertTrue(hasattr(data, creg.name)) self._assert_allclose(getattr(data, creg.name), np.array(target[creg.name])) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V1) + def test_circuit_with_multiple_cregs_v1(self, backend): + """Test for circuit with multiple classical registers.""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + cases = [] + + # case 1 + a = ClassicalRegister(1, "a") + b = ClassicalRegister(2, "b") + c = ClassicalRegister(3, "c") + + qc = QuantumCircuit(QuantumRegister(3), a, b, c) + qc.h(range(3)) + qc.measure([0, 1, 2, 2], [0, 2, 4, 5]) + qc = pm.run(qc) + target = {"a": {0: 5000, 1: 5000}, "b": {0: 5000, 2: 5000}, "c": {0: 5000, 6: 5000}} + cases.append(("use all cregs", qc, target)) + + # case 2 + a = ClassicalRegister(1, "a") + b = ClassicalRegister(5, "b") + c = ClassicalRegister(3, "c") + + qc = QuantumCircuit(QuantumRegister(3), a, b, c) + qc.h(range(3)) + qc.measure([0, 1, 2, 2], [0, 2, 4, 5]) + qc = pm.run(qc) + target = { + "a": {0: 5000, 1: 5000}, + "b": {0: 2500, 2: 2500, 24: 2500, 26: 2500}, + "c": {0: 10000}, + } + cases.append(("use only a and b", qc, target)) + + # case 3 + a = ClassicalRegister(1, "a") + b = ClassicalRegister(2, "b") + c = ClassicalRegister(3, "c") + + qc = QuantumCircuit(QuantumRegister(3), a, b, c) + qc.h(range(3)) + qc.measure(1, 5) + qc = pm.run(qc) + target = {"a": {0: 10000}, "b": {0: 10000}, "c": {0: 5000, 4: 5000}} + cases.append(("use only c", qc, target)) + + # case 4 + a = ClassicalRegister(1, "a") + b = ClassicalRegister(2, "b") + c = ClassicalRegister(3, "c") + + qc = QuantumCircuit(QuantumRegister(3), a, b, c) + qc.h(range(3)) + qc.measure([0, 1, 2], [5, 5, 5]) + qc = pm.run(qc) + target = {"a": {0: 10000}, "b": {0: 10000}, "c": {0: 5000, 4: 5000}} + cases.append(("use only c multiple qubits", qc, target)) + + # case 5 + a = ClassicalRegister(1, "a") + b = ClassicalRegister(2, "b") + c = ClassicalRegister(3, "c") + + qc = QuantumCircuit(QuantumRegister(3), a, b, c) + qc.h(range(3)) + qc = pm.run(qc) + target = {"a": {0: 10000}, "b": {0: 10000}, "c": {0: 10000}} + cases.append(("no measure", qc, target)) + + for title, qc, target in cases: + with self.subTest(title): + sampler = BackendSamplerV2(backend=backend, options=self._options) + result = sampler.run([qc], shots=self._shots).result() + self.assertEqual(len(result), 1) + data = result[0].data + self.assertEqual(len(data), 3) + for creg in qc.cregs: + self.assertTrue(hasattr(data, creg.name)) + self._assert_allclose(getattr(data, creg.name), np.array(target[creg.name])) + + @combine(backend=BACKENDS_V2) def test_circuit_with_aliased_cregs(self, backend): """Test for circuit with aliased classical registers.""" q = QuantumRegister(3, "q") @@ -644,6 +1237,47 @@ def test_circuit_with_aliased_cregs(self, backend): self.assertTrue(hasattr(data, creg_name)) self._assert_allclose(getattr(data, creg_name), np.array(creg)) + @combine(backend=BACKENDS_V1) + def test_circuit_with_aliased_cregs_v1(self, backend): + """Test for circuit with aliased classical registers.""" + q = QuantumRegister(3, "q") + c1 = ClassicalRegister(1, "c1") + c2 = ClassicalRegister(1, "c2") + + qc = QuantumCircuit(q, c1, c2) + qc.ry(np.pi / 4, 2) + qc.cx(2, 1) + qc.cx(0, 1) + qc.h(0) + qc.measure(0, c1) + qc.measure(1, c2) + qc.z(2).c_if(c1, 1) + qc.x(2).c_if(c2, 1) + qc2 = QuantumCircuit(5, 5) + qc2.compose(qc, [0, 2, 3], [2, 4], inplace=True) + cregs = [creg.name for creg in qc2.cregs] + target = { + cregs[0]: {0: 4255, 4: 4297, 16: 720, 20: 726}, + cregs[1]: {0: 5000, 1: 5000}, + cregs[2]: {0: 8500, 1: 1500}, + } + + sampler = BackendSamplerV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc2 = pm.run(qc2) + result = sampler.run([qc2], shots=self._shots).result() + self.assertEqual(len(result), 1) + data = result[0].data + self.assertEqual(len(data), 3) + for creg_name, creg in target.items(): + self.assertTrue(hasattr(data, creg_name)) + self._assert_allclose(getattr(data, creg_name), np.array(creg)) + @combine(backend=BACKENDS) def test_no_cregs(self, backend): """Test that the sampler works when there are no classical register in the circuit.""" @@ -670,7 +1304,7 @@ def test_empty_creg(self, backend): result = sampler.run([qc], shots=self._shots).result() self.assertEqual(result[0].data.c1.array.shape, (self._shots, 0)) - @combine(backend=BACKENDS) + @combine(backend=BACKENDS_V2) def test_diff_shots(self, backend): """Test of pubs with different shots""" pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -688,6 +1322,29 @@ def test_diff_shots(self, backend): self.assertEqual(result[1].data.meas.num_shots, shots2) self._assert_allclose(result[1].data.meas, np.array(target2)) + @combine(backend=BACKENDS_V1) + def test_diff_shots_v1(self, backend): + """Test of pubs with different shots""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + bell, _, target = self._cases[1] + bell = pm.run(bell) + sampler = BackendSamplerV2(backend=backend, options=self._options) + shots2 = self._shots + 2 + target2 = {k: v + 1 for k, v in target.items()} + job = sampler.run([(bell, None, self._shots), (bell, None, shots2)]) + result = job.result() + self.assertEqual(len(result), 2) + self.assertEqual(result[0].data.meas.num_shots, self._shots) + self._assert_allclose(result[0].data.meas, np.array(target)) + self.assertEqual(result[1].data.meas.num_shots, shots2) + self._assert_allclose(result[1].data.meas, np.array(target2)) + def test_job_size_limit_backend_v2(self): """Test BackendSamplerV2 respects backend's job size limit.""" diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index d743c4be5c53..6d8716359bcc 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -130,12 +130,15 @@ def test_circuit_on_fake_backend(self, backend, optimization_level): self.skipTest( f"Unable to run fake_backend {backend.configuration().backend_name} without qiskit-aer" ) - job = backend.run( - transpile( + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled = transpile( self.circuit, backend, seed_transpiler=42, optimization_level=optimization_level - ), - seed_simulator=42, - ) + ) + job = backend.run(transpiled, seed_simulator=42) result = job.result() counts = result.get_counts() max_count = max(counts.items(), key=operator.itemgetter(1))[0] @@ -143,15 +146,18 @@ def test_circuit_on_fake_backend(self, backend, optimization_level): def test_qobj_failure(self): backend = BACKENDS[-1] - tqc = transpile(self.circuit, backend) with self.assertWarns(DeprecationWarning): + tqc = transpile(self.circuit, backend) qobj = assemble(tqc, backend) with self.assertRaises(QiskitError): backend.run(qobj) @data(*BACKENDS) def test_to_dict_properties(self, backend): - properties = backend.properties() + with warnings.catch_warnings(): + # The class QobjExperimentHeader is deprecated + warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") + properties = backend.properties() if properties: self.assertIsInstance(backend.properties().to_dict(), dict) else: @@ -220,7 +226,12 @@ def test_delay_circuit(self): qc.x(1) qc.delay(250, 1, unit="ns") qc.measure_all() - res = transpile(qc, backend) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + res = transpile(qc, backend) self.assertIn("delay", res.count_ops()) @data(0, 1, 2, 3) @@ -529,10 +540,10 @@ def test_filter_faulty_qubits_backend_v2_converter(self): """Test faulty qubits in v2 conversion.""" with self.assertWarns(DeprecationWarning): backend = Fake127QPulseV1() - # Get properties dict to make it easier to work with the properties API - # is difficult to edit because of the multiple layers of nesting and - # different object types - props_dict = backend.properties().to_dict() + # Get properties dict to make it easier to work with the properties API + # is difficult to edit because of the multiple layers of nesting and + # different object types + props_dict = backend.properties().to_dict() for i in range(62, 67): non_operational = { "date": datetime.datetime.now(datetime.timezone.utc), @@ -541,7 +552,8 @@ def test_filter_faulty_qubits_backend_v2_converter(self): "value": 0, } props_dict["qubits"][i].append(non_operational) - backend._properties = BackendProperties.from_dict(props_dict) + with self.assertWarns(DeprecationWarning): + backend._properties = BackendProperties.from_dict(props_dict) v2_backend = BackendV2Converter(backend, filter_faulty=True) for i in range(62, 67): for qarg in v2_backend.target.qargs: @@ -551,10 +563,10 @@ def test_filter_faulty_qubits_backend_v2_converter_with_delay(self): """Test faulty qubits in v2 conversion.""" with self.assertWarns(DeprecationWarning): backend = Fake127QPulseV1() - # Get properties dict to make it easier to work with the properties API - # is difficult to edit because of the multiple layers of nesting and - # different object types - props_dict = backend.properties().to_dict() + # Get properties dict to make it easier to work with the properties API + # is difficult to edit because of the multiple layers of nesting and + # different object types + props_dict = backend.properties().to_dict() for i in range(62, 67): non_operational = { "date": datetime.datetime.now(datetime.timezone.utc), @@ -563,7 +575,8 @@ def test_filter_faulty_qubits_backend_v2_converter_with_delay(self): "value": 0, } props_dict["qubits"][i].append(non_operational) - backend._properties = BackendProperties.from_dict(props_dict) + with self.assertWarns(DeprecationWarning): + backend._properties = BackendProperties.from_dict(props_dict) v2_backend = BackendV2Converter(backend, filter_faulty=True, add_delay=True) for i in range(62, 67): for qarg in v2_backend.target.qargs: @@ -602,9 +615,9 @@ def test_backend_v2_converter_with_meaningless_gate_config(self): """Test backend with broken gate config can be converted only with properties data.""" with self.assertWarns(DeprecationWarning): backend_v1 = Fake5QV1() - backend_v1.configuration().gates = [ - GateConfig(name="NotValidGate", parameters=[], qasm_def="not_valid_gate") - ] + backend_v1.configuration().gates = [ + GateConfig(name="NotValidGate", parameters=[], qasm_def="not_valid_gate") + ] backend_v2 = BackendV2Converter( backend=backend_v1, filter_faulty=True, @@ -620,10 +633,10 @@ def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): """Test faulty gates and qubits.""" with self.assertWarns(DeprecationWarning): backend = Fake127QPulseV1() - # Get properties dict to make it easier to work with the properties API - # is difficult to edit because of the multiple layers of nesting and - # different object types - props_dict = backend.properties().to_dict() + # Get properties dict to make it easier to work with the properties API + # is difficult to edit because of the multiple layers of nesting and + # different object types + props_dict = backend.properties().to_dict() for i in range(62, 67): non_operational = { "date": datetime.datetime.now(datetime.timezone.utc), @@ -652,7 +665,8 @@ def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): if tuple(gate["qubits"]) in invalid_cx_edges: gate["parameters"].append(non_operational_gate) - backend._properties = BackendProperties.from_dict(props_dict) + with self.assertWarns(DeprecationWarning): + backend._properties = BackendProperties.from_dict(props_dict) v2_backend = BackendV2Converter(backend, filter_faulty=True) for i in range(62, 67): for qarg in v2_backend.target.qargs: @@ -688,7 +702,8 @@ def test_filter_faulty_gates_v2_converter(self): if tuple(gate["qubits"]) in invalid_cx_edges: gate["parameters"].append(non_operational_gate) - backend._properties = BackendProperties.from_dict(props_dict) + with self.assertWarns(DeprecationWarning): + backend._properties = BackendProperties.from_dict(props_dict) v2_backend = BackendV2Converter(backend, filter_faulty=True) for i in range(62, 67): self.assertIn((i,), v2_backend.target.qargs) @@ -699,7 +714,7 @@ def test_filter_faulty_no_faults_v2_converter(self): """Test that faulty qubit filtering does nothing with all operational qubits and gates.""" with self.assertWarns(DeprecationWarning): backend = Fake127QPulseV1() - v2_backend = BackendV2Converter(backend, filter_faulty=True) + v2_backend = BackendV2Converter(backend, filter_faulty=True) for i in range(v2_backend.num_qubits): self.assertIn((i,), v2_backend.target.qargs) @@ -707,17 +722,19 @@ def test_filter_faulty_no_faults_v2_converter(self): def test_faulty_full_path_transpile_connected_cmap(self, opt_level): with self.assertWarns(DeprecationWarning): backend = Fake5QV1() + props = backend.properties().to_dict() + non_operational_gate = { "date": datetime.datetime.now(datetime.timezone.utc), "name": "operational", "unit": "", "value": 0, } - props = backend.properties().to_dict() for gate in props["gates"]: if tuple(sorted(gate["qubits"])) == (0, 1): gate["parameters"].append(non_operational_gate) - backend._properties = BackendProperties.from_dict(props) + with self.assertWarns(DeprecationWarning): + backend._properties = BackendProperties.from_dict(props) v2_backend = BackendV2Converter(backend, filter_faulty=True) qc = QuantumCircuit(5) for x, y in itertools.product(range(5), range(5)): diff --git a/test/python/providers/test_faulty_backend.py b/test/python/providers/test_faulty_backend.py index e1a3f9fa179e..bd8db856ec0b 100644 --- a/test/python/providers/test_faulty_backend.py +++ b/test/python/providers/test_faulty_backend.py @@ -26,15 +26,21 @@ class FaultyQubitBackendTestCase(QiskitTestCase): """Test operational-related methods of backend.properties() with Fake7QV1FaultyQ1, which is like Fake7QV1 but with a faulty 1Q""" + # These test can be removed with Fake7QV1FaultyQ1 + backend = Fake7QV1FaultyQ1() def test_operational_false(self): """Test operation status of the qubit. Q1 is non-operational""" - self.assertFalse(self.backend.properties().is_qubit_operational(1)) + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + self.assertFalse(properties.is_qubit_operational(1)) def test_faulty_qubits(self): """Test faulty_qubits method.""" - self.assertEqual(self.backend.properties().faulty_qubits(), [1]) + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + self.assertEqual(properties.faulty_qubits(), [1]) def test_convert_to_target_with_filter(self): """Test converting legacy data structure to V2 target model with faulty qubits. @@ -43,11 +49,13 @@ def test_convert_to_target_with_filter(self): even though instruction is not provided by the backend, since these are the necessary instructions that the transpiler may assume. """ + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() # Filter out faulty Q1 target = convert_to_target( configuration=self.backend.configuration(), - properties=self.backend.properties(), + properties=properties, add_delay=True, filter_faulty=True, ) @@ -57,10 +65,13 @@ def test_convert_to_target_with_filter(self): def test_convert_to_target_without_filter(self): """Test converting legacy data structure to V2 target model with faulty qubits.""" + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + # Include faulty Q1 even though data could be incomplete target = convert_to_target( configuration=self.backend.configuration(), - properties=self.backend.properties(), + properties=properties, add_delay=True, filter_faulty=False, ) @@ -68,17 +79,20 @@ def test_convert_to_target_without_filter(self): self.assertTrue(target.instruction_supported(operation_name="delay", qargs=(1,))) # Properties are preserved + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + self.assertEqual( target.qubit_properties[1].t1, - self.backend.properties().t1(1), + properties.t1(1), ) self.assertEqual( target.qubit_properties[1].t2, - self.backend.properties().t2(1), + properties.t2(1), ) self.assertEqual( target.qubit_properties[1].frequency, - self.backend.properties().frequency(1), + properties.frequency(1), ) @@ -90,12 +104,16 @@ class FaultyGate13BackendTestCase(QiskitTestCase): def test_operational_gate(self): """Test is_gate_operational method.""" - self.assertFalse(self.backend.properties().is_gate_operational("cx", [1, 3])) - self.assertFalse(self.backend.properties().is_gate_operational("cx", [3, 1])) + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + self.assertFalse(properties.is_gate_operational("cx", [1, 3])) + self.assertFalse(properties.is_gate_operational("cx", [3, 1])) def test_faulty_gates(self): """Test faulty_gates method.""" - gates = self.backend.properties().faulty_gates() + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + gates = properties.faulty_gates() self.assertEqual(len(gates), 2) self.assertEqual([gate.gate for gate in gates], ["cx", "cx"]) self.assertEqual(sorted(gate.qubits for gate in gates), [[1, 3], [3, 1]]) @@ -109,12 +127,16 @@ class FaultyGate01BackendTestCase(QiskitTestCase): def test_operational_gate(self): """Test is_gate_operational method.""" - self.assertFalse(self.backend.properties().is_gate_operational("cx", [0, 1])) - self.assertFalse(self.backend.properties().is_gate_operational("cx", [1, 0])) + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + self.assertFalse(properties.is_gate_operational("cx", [0, 1])) + self.assertFalse(properties.is_gate_operational("cx", [1, 0])) def test_faulty_gates(self): """Test faulty_gates method.""" - gates = self.backend.properties().faulty_gates() + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + gates = properties.faulty_gates() self.assertEqual(len(gates), 2) self.assertEqual([gate.gate for gate in gates], ["cx", "cx"]) self.assertEqual(sorted(gate.qubits for gate in gates), [[0, 1], [1, 0]]) @@ -129,9 +151,12 @@ class MissingPropertyQubitBackendTestCase(QiskitTestCase): def test_convert_to_target(self): """Test converting legacy data structure to V2 target model with missing qubit property.""" + with self.assertWarns(DeprecationWarning): + properties = self.backend.properties() + target = convert_to_target( configuration=self.backend.configuration(), - properties=self.backend.properties(), + properties=properties, add_delay=True, filter_faulty=True, ) @@ -139,9 +164,9 @@ def test_convert_to_target(self): self.assertIsNone(target.qubit_properties[1].t1) self.assertEqual( target.qubit_properties[1].t2, - self.backend.properties().t2(1), + properties.t2(1), ) self.assertEqual( target.qubit_properties[1].frequency, - self.backend.properties().frequency(1), + properties.frequency(1), ) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 563b83345496..72a6de11ae08 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -766,9 +766,13 @@ def get_sched(qubit_idx: [int], backend): qc = circuit.QuantumCircuit(2) for idx in qubit_idx: qc.append(circuit.library.U2Gate(0, pi / 2), [idx]) - return compiler.schedule( - compiler.transpile(qc, backend=backend, optimization_level=1), backend - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled = compiler.transpile(qc, backend=backend, optimization_level=1) + return compiler.schedule(transpiled, backend) with pulse.build(self.backend) as schedule: with pulse.align_sequential(): @@ -788,7 +792,12 @@ def get_sched(qubit_idx: [int], backend): # prepare and schedule circuits that will be used. single_u2_qc = circuit.QuantumCircuit(2) single_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) single_u2_sched = compiler.schedule(single_u2_qc, self.backend) # sequential context @@ -813,7 +822,12 @@ def get_sched(qubit_idx: [int], backend): triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") # measurement diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index cb7293049ef5..90676a38a586 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -267,6 +267,8 @@ def build_reverse( def test_rzx_calibration_cr_pulse_stretch(self, theta: float): """Test that cross resonance pulse durations are computed correctly.""" with self.assertWarns(DeprecationWarning): + # TODO this tests does not work with BackendV2/GenericBackendV2 + # https://github.com/Qiskit/qiskit/issues/12834 backend = Fake27QPulseV1() inst_map = backend.defaults().instruction_schedule_map cr_schedule = inst_map.get("cx", (0, 1)) @@ -514,7 +516,7 @@ def test_raises_error_when_rotation_angle_not_assigned(self): an unassigned Parameter, not a number. The QiskitError occurs while trying to typecast the Parameter into a float. """ - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) tp = RXCalibrationBuilder(backend.target) qubits = (0,) rx = RXGate(Parameter("theta")) @@ -526,7 +528,7 @@ def test_raises_error_when_rotation_angle_not_assigned(self): @data(0, np.pi / 3, (2 / 3) * np.pi) def test_pulse_schedule(self, theta: float): """Test that get_calibration() returns a schedule with correct amplitude.""" - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) dummy_target = Target() sx_amp, sx_beta, sx_sigma, sx_duration, sx_angle = 0.6, 2, 40, 160, 0.5 with builder.build(backend=backend) as dummy_sx_cal: @@ -577,7 +579,7 @@ def test_with_normalizerxangles(self): ) ism = InstructionScheduleMap() ism.add("sx", (0,), sched) - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism) + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism, seed=42) # NormalizeRXAngle pass should also be included because it's a required pass. pm = PassManager(RXCalibrationBuilder(backend.target)) diff --git a/test/python/transpiler/test_gates_in_basis_pass.py b/test/python/transpiler/test_gates_in_basis_pass.py index 06ce5e0f6702..0dd5e880e0c0 100644 --- a/test/python/transpiler/test_gates_in_basis_pass.py +++ b/test/python/transpiler/test_gates_in_basis_pass.py @@ -99,7 +99,7 @@ def test_all_gates_in_basis_after_translation(self): def test_all_gates_in_basis_with_target(self): """Test circuit with all gates in basis with target.""" - target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target + target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"], seed=42).target basis_gates = ["cx", "u"] # not used property_set = {} analysis_pass = GatesInBasis(basis_gates, target=target) @@ -112,7 +112,7 @@ def test_all_gates_in_basis_with_target(self): def test_all_gates_not_in_basis_with_target(self): """Test circuit with not all gates in basis with target.""" - target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target + target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"], seed=42).target basis_gates = ["cx", "h"] property_set = {} analysis_pass = GatesInBasis(basis_gates, target=target) @@ -125,7 +125,7 @@ def test_all_gates_not_in_basis_with_target(self): def test_all_gates_in_basis_not_on_all_qubits_with_target(self): """Test circuit with gate in global basis but not local basis.""" - target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target + target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"], seed=42).target basis_gates = ["ecr", "cx", "h"] property_set = {} analysis_pass = GatesInBasis(basis_gates, target=target) @@ -138,7 +138,7 @@ def test_all_gates_in_basis_not_on_all_qubits_with_target(self): def test_all_gates_in_basis_empty_circuit_with_target(self): """Test circuit with no gates with target.""" - target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target + target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"], seed=42).target basis_gates = ["cx", "u"] property_set = {} analysis_pass = GatesInBasis(basis_gates, target=target) @@ -191,7 +191,7 @@ def test_all_gates_not_in_ideal_sim_target(self): def test_all_gates_in_basis_after_translation_with_target(self): """Test circuit with gates in basis after conditional translation.""" - target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target + target = GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"], seed=42).target basis_gates = ["cx", "u"] property_set = {} analysis_pass = GatesInBasis(basis_gates, target) diff --git a/test/python/transpiler/test_passmanager_run.py b/test/python/transpiler/test_passmanager_run.py index 3da6a042564c..96df928c6e21 100644 --- a/test/python/transpiler/test_passmanager_run.py +++ b/test/python/transpiler/test_passmanager_run.py @@ -85,7 +85,10 @@ def test_default_pass_manager_single(self): circuit.cx(qr[2], qr[3]) backend = GenericBackendV2( - num_qubits=20, coupling_map=ALMADEN_CMAP, basis_gates=["id", "u1", "u2", "u3", "cx"] + num_qubits=20, + coupling_map=ALMADEN_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, ) initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] @@ -187,7 +190,10 @@ def test_default_pass_manager_two(self): initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] backend = GenericBackendV2( - num_qubits=20, coupling_map=coupling_map, basis_gates=["id", "u1", "u2", "u3", "cx"] + num_qubits=20, + coupling_map=coupling_map, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, ) pass_manager = level_1_pass_manager( diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 3d11a5d55e93..58f6d35a20d5 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -39,7 +39,7 @@ from qiskit.transpiler.preset_passmanagers.builtin_plugins import OptimizationPassManager from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, LAGOS_CMAP, TOKYO_CMAP +from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, LAGOS_CMAP, TOKYO_CMAP, BOGOTA_CMAP def mock_get_passmanager_stage( @@ -222,8 +222,12 @@ def test_alignment_constraints_called_with_by_default(self, level): circuit.h(q[0]) circuit.cz(q[0], q[1]) with unittest.mock.patch("qiskit.transpiler.passes.TimeUnitConversion.run") as mock: - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) transpile(circuit, backend=backend, optimization_level=level) mock.assert_not_called() @@ -238,15 +242,23 @@ def test_alignment_constraints_called_with_delay_in_circuit(self, level): with unittest.mock.patch( "qiskit.transpiler.passes.TimeUnitConversion.run", return_value=circuit_to_dag(circuit) ) as mock: - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) transpile(circuit, backend=backend, optimization_level=level) mock.assert_called_once() def test_unroll_only_if_not_gates_in_basis(self): """Test that the list of passes _unroll only runs if a gate is not in the basis.""" - with self.assertWarns(DeprecationWarning): - qcomp = Fake5QV1() + qcomp = GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) qv_circuit = QuantumVolume(3) gates_in_basis_true_count = 0 collect_2q_blocks_count = 0 @@ -278,8 +290,18 @@ class TestTranspileLevels(QiskitTestCase): circuit=[emptycircuit, circuit_2532], level=[0, 1, 2, 3], backend=[ - Fake5QV1(), - Fake20QV1(), + GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ), + GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ), None, ], dsc="Transpiler {circuit.__name__} on {backend} backend at level {level}", @@ -290,6 +312,28 @@ def test(self, circuit, level, backend): result = transpile(circuit(), backend=backend, optimization_level=level, seed_transpiler=42) self.assertIsInstance(result, QuantumCircuit) + @combine( + circuit=[emptycircuit, circuit_2532], + level=[0, 1, 2, 3], + backend=[ + Fake5QV1(), + Fake20QV1(), + ], + dsc="Transpiler {circuit.__name__} on {backend} backend V1 at level {level}", + name="{circuit.__name__}_{backend}_level{level}", + ) + def test_v1(self, circuit, level, backend): + """All the levels with all the backends""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + result = transpile( + circuit(), backend=backend, optimization_level=level, seed_transpiler=42 + ) + self.assertIsInstance(result, QuantumCircuit) + @ddt class TestPassesInspection(QiskitTestCase): @@ -787,8 +831,12 @@ def test_layout_2503(self, level): 19: ancilla[16], } - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) qc_b = transpile(qc, backend, initial_layout=initial_layout, optimization_level=level) self.assertEqual(qc_b._layout.initial_layout._p2v, final_layout) @@ -1044,8 +1092,12 @@ def test_trivial_layout(self, level): expected_layouts = [trivial_layout, trivial_layout] - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) result = transpile(qc, backend, optimization_level=level, seed_transpiler=42) self.assertEqual(result._layout.initial_layout._p2v, expected_layouts[level]) @@ -1078,8 +1130,12 @@ def test_initial_layout(self, level): 18: qr[9], } - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) result = transpile( qc, backend, optimization_level=level, initial_layout=initial_layout, seed_transpiler=42 ) @@ -1159,8 +1215,12 @@ def test_optimization_condition(self, level): cr = ClassicalRegister(1) qc = QuantumCircuit(qr, cr) qc.cx(0, 1).c_if(cr, 1) - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) circ = transpile(qc, backend, optimization_level=level) self.assertIsInstance(circ, QuantumCircuit) @@ -1226,7 +1286,12 @@ def test_with_backend(self, optimization_level): """Test a passmanager is constructed when only a backend and optimization level.""" with self.assertWarns(DeprecationWarning): backend = Fake20QV1() - pm = generate_preset_pass_manager(optimization_level, backend) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `generate_preset_pass_manager` function will " + "stop supporting inputs of type `BackendV1`", + ): + pm = generate_preset_pass_manager(optimization_level, backend) self.assertIsInstance(pm, PassManager) def test_default_optimization_level(self): @@ -1597,9 +1662,12 @@ def test_invalid_methods_raise_on_control_flow(self, optimization_level): def test_unsupported_basis_gates_raise(self, optimization_level): """Test that trying to transpile a control-flow circuit for a backend that doesn't support the necessary operations in its `basis_gates` will raise a sensible error.""" - with self.assertWarns(DeprecationWarning): - backend = Fake20QV1() - + backend = GenericBackendV2( + num_qubits=20, + coupling_map=TOKYO_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) qc = QuantumCircuit(1, 1) with qc.for_loop((0,)): pass diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 539674609c26..07d6172264d4 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -57,6 +57,7 @@ def setUp(self): def test_transpile_with_bare_backend(self): """Test transpile without custom calibrations.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() # Remove timing constraints to avoid triggering # scheduling passes. @@ -69,7 +70,12 @@ def test_transpile_with_bare_backend(self): qc.sx(1) qc.measure_all() - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) ref_calibration = {} self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) @@ -78,9 +84,7 @@ def test_transpile_with_backend_target(self): """Test transpile without custom calibrations from target.""" target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=True, + num_qubits=5, coupling_map=BOGOTA_CMAP, calibrate_instructions=True, seed=42 ).target qc = circuit.QuantumCircuit(2) @@ -98,6 +102,7 @@ def test_transpile_with_backend_target(self): def test_transpile_with_custom_basis_gate(self): """Test transpile with custom calibrations.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) @@ -112,7 +117,12 @@ def test_transpile_with_custom_basis_gate(self): qc.sx(1) qc.measure_all() - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) ref_calibration = { "sx": { @@ -156,6 +166,7 @@ def test_transpile_with_custom_basis_gate_in_target(self): def test_transpile_with_instmap(self): """Test providing instruction schedule map.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) @@ -163,6 +174,7 @@ def test_transpile_with_instmap(self): # Inst map is renewed with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() # Remove timing constraints to avoid triggering # scheduling passes. @@ -175,7 +187,12 @@ def test_transpile_with_instmap(self): qc.sx(1) qc.measure_all() - transpiled_qc = transpile(qc, backend, inst_map=instmap, initial_layout=[0, 1]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, inst_map=instmap, initial_layout=[0, 1]) ref_calibration = { "sx": { @@ -188,6 +205,7 @@ def test_transpile_with_instmap(self): def test_transpile_with_custom_gate(self): """Test providing non-basis gate.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] @@ -197,9 +215,10 @@ def test_transpile_with_custom_gate(self): ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,), (1,)] - ) + with self.assertWarns(DeprecationWarning): + dummy_config = GateConfig( + name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,), (1,)] + ) backend.configuration().gates.append(dummy_config) # Remove timing constraints to avoid triggering # scheduling passes. @@ -209,7 +228,12 @@ def test_transpile_with_custom_gate(self): qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) qc.append(circuit.Gate("my_gate", 1, [2.0]), [1]) - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0, 1]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0, 1]) my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) my_gate_q1_2_0 = self.my_gate_q1.assign_parameters({self.sched_param: 2.0}, inplace=False) @@ -225,13 +249,17 @@ def test_transpile_with_custom_gate(self): def test_transpile_with_parameterized_custom_gate(self): """Test providing non-basis gate, which is kept parameterized throughout transpile.""" with self.assertWarns(DeprecationWarning): + # TODO convert this to BackendV2/Target backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") - dummy_config = GateConfig(name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)]) + with self.assertWarns(DeprecationWarning): + dummy_config = GateConfig( + name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] + ) backend.configuration().gates.append(dummy_config) # Remove timing constraints to avoid triggering # scheduling passes. @@ -241,7 +269,12 @@ def test_transpile_with_parameterized_custom_gate(self): qc = circuit.QuantumCircuit(1) qc.append(circuit.Gate("my_gate", 1, [param]), [0]) - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) my_gate_q0_p = self.my_gate_q0.assign_parameters({self.sched_param: param}, inplace=False) @@ -255,13 +288,17 @@ def test_transpile_with_parameterized_custom_gate(self): def test_transpile_with_multiple_circuits(self): """Test transpile with multiple circuits with custom gate.""" with self.assertWarns(DeprecationWarning): + # TODO move this test to backendV2 backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") - dummy_config = GateConfig(name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)]) + with self.assertWarns(DeprecationWarning): + dummy_config = GateConfig( + name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] + ) backend.configuration().gates.append(dummy_config) # Remove timing constraints to avoid triggering # scheduling passes. @@ -274,7 +311,12 @@ def test_transpile_with_multiple_circuits(self): qc.append(circuit.Gate("my_gate", 1, [param]), [0]) circs.append(qc) - transpiled_qcs = transpile(circs, backend, basis_gates=["my_gate"], initial_layout=[0]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qcs = transpile(circs, backend, basis_gates=["my_gate"], initial_layout=[0]) for param, transpiled_qc in zip(params, transpiled_qcs): my_gate_q0_x = self.my_gate_q0.assign_parameters( @@ -286,13 +328,17 @@ def test_transpile_with_multiple_circuits(self): def test_multiple_instructions_with_different_parameters(self): """Test adding many instruction with different parameter binding.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add( "my_gate", (0,), self.my_gate_q0, arguments=["P0"] ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") - dummy_config = GateConfig(name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)]) + with self.assertWarns(DeprecationWarning): + dummy_config = GateConfig( + name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] + ) backend.configuration().gates.append(dummy_config) # Remove timing constraints to avoid triggering # scheduling passes. @@ -303,7 +349,12 @@ def test_multiple_instructions_with_different_parameters(self): qc.append(circuit.Gate("my_gate", 1, [2.0]), [0]) qc.append(circuit.Gate("my_gate", 1, [3.0]), [0]) - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) my_gate_q0_2_0 = self.my_gate_q0.assign_parameters({self.sched_param: 2.0}, inplace=False) @@ -321,6 +372,7 @@ def test_multiple_instructions_with_different_parameters(self): def test_transpile_with_different_qubit(self): """Test transpile with qubit without custom gate.""" with self.assertWarns(DeprecationWarning): + # TODO Move this test to backendV2 backend = Fake27QPulseV1() backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) # Remove timing constraints to avoid triggering @@ -331,7 +383,12 @@ def test_transpile_with_different_qubit(self): qc.sx(0) qc.measure_all() - transpiled_qc = transpile(qc, backend, initial_layout=[3]) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + transpiled_qc = transpile(qc, backend, initial_layout=[3]) self.assertDictEqual(transpiled_qc.calibrations, {}) @@ -449,10 +506,7 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) qc.append(gate, [0]) qc.measure_all() - backend = GenericBackendV2( - num_qubits=5, - calibrate_instructions=True, - ) + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) transpiled_qc = transpile( qc, backend, @@ -475,10 +529,7 @@ def test_transpile_with_instmap_not_mutate_backend(self): This should not override the source object since the same backend may be used for future transpile without intention of instruction overriding. """ - backend = GenericBackendV2( - num_qubits=5, - calibrate_instructions=True, - ) + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) original_sx0 = backend.target["sx"][(0,)].calibration with self.assertWarns(DeprecationWarning): diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 42ae3691c3ca..5ab8fe5c10b2 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -24,12 +24,12 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.compiler.transpiler import transpile -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import ALMADEN_CMAP +from ..legacy_cmaps import ALMADEN_CMAP, MUMBAI_CMAP class TestSabreLayout(QiskitTestCase): @@ -195,8 +195,12 @@ def test_layout_with_classical_bits(self): rz(0) q4835[1]; """ ) - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() + backend = GenericBackendV2( + num_qubits=27, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=MUMBAI_CMAP, + seed=42, + ) res = transpile( qc, backend, layout_method="sabre", seed_transpiler=1234, optimization_level=1 ) @@ -249,8 +253,12 @@ def test_layout_many_search_trials(self): barrier q18585[5],q18585[2],q18585[8],q18585[3],q18585[6]; """ ) - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() + backend = GenericBackendV2( + num_qubits=27, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=MUMBAI_CMAP, + seed=42, + ) res = transpile( qc, backend, diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 6650ca27c6f4..856b5ff09f5b 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -1393,7 +1393,7 @@ def test_random_circuit_no_control_flow_target(self, size): routing_method="sabre", layout_method="sabre", seed_transpiler=12342, - target=GenericBackendV2(num_qubits=27, coupling_map=MUMBAI_CMAP).target, + target=GenericBackendV2(num_qubits=27, coupling_map=MUMBAI_CMAP, seed=42).target, ) self.assert_valid_circuit(tqc) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 43acd4ef67af..4abf6511d8d2 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -65,6 +65,7 @@ from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) +from ..legacy_cmaps import YORKTOWN_CMAP class FakeBackend2QV2(GenericBackendV2): @@ -674,8 +675,14 @@ def test_coupling_map_unequal_durations(self, opt): qr = QuantumRegister(2) circ = QuantumCircuit(qr) circ.append(random_unitary(4, seed=1), [1, 0]) - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() + backend = GenericBackendV2( + num_qubits=5, + coupling_map=YORKTOWN_CMAP, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + calibrate_instructions=True, + pulse_channels=True, + seed=42, + ) tqc = transpile( circ, backend=backend, @@ -687,7 +694,7 @@ def test_coupling_map_unequal_durations(self, opt): self.assertTrue( all( ( - (0, 1) == (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]) + (1, 0) == (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]) for instr in tqc.get_instructions("cx") ) ) @@ -908,7 +915,7 @@ def test_single_qubit_with_target(self): qc = QuantumCircuit(1) qc.append(ZGate(), [qc.qubits[0]]) dag = circuit_to_dag(qc) - backend = GenericBackendV2(num_qubits=5) + backend = GenericBackendV2(num_qubits=5, seed=42) unitary_synth_pass = UnitarySynthesis(target=backend.target) result_dag = unitary_synth_pass.run(dag) result_qc = dag_to_circuit(result_dag) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index e97ed279a8d8..5acdd3ba6ebc 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -26,7 +26,7 @@ from qiskit.transpiler.target import Target, InstructionProperties from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import LIMA_CMAP, YORKTOWN_CMAP +from ..legacy_cmaps import LIMA_CMAP, YORKTOWN_CMAP, BOGOTA_CMAP class TestVF2PostLayout(QiskitTestCase): @@ -183,29 +183,6 @@ def test_skip_3q_circuit_control_flow_v2(self): vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q ) - def test_best_mapping_ghz_state_full_device_multiple_qregs(self): - """Test best mappings with multiple registers""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qr_a = QuantumRegister(2) - qr_b = QuantumRegister(3) - qc = QuantumCircuit(qr_a, qr_b) - qc.h(qr_a[0]) - qc.cx(qr_a[0], qr_a[1]) - qc.cx(qr_a[0], qr_b[0]) - qc.cx(qr_a[0], qr_b[1]) - qc.cx(qr_a[0], qr_b[2]) - qc.measure_all() - tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - def test_2q_circuit_5q_backend(self): """A simple example, without considering the direction 0 - 1 @@ -217,7 +194,12 @@ def test_2q_circuit_5q_backend(self): qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - tqc = transpile(circuit, backend, layout_method="dense") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) cmap = CouplingMap(backend.configuration().coupling_map) @@ -254,6 +236,40 @@ def test_2q_circuit_5q_backend_controlflow(self): self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_2q_circuit_5q_backend_max_trials(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + max_trials = 11 + backend = GenericBackendV2( + num_qubits=5, + coupling_map=YORKTOWN_CMAP, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + seed=1, + ) + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.coupling_map) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed, max_trials=max_trials) + with self.assertLogs( + "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" + ) as cm: + pass_.run(dag) + self.assertIn( + f"DEBUG:qiskit.transpiler.passes.layout.vf2_post_layout:Trial {max_trials} " + f"is >= configured max trials {max_trials}", + cm.output, + ) + print(pass_.property_set["VF2PostLayout_stop_reason"]) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend_max_trials_v1(self): """A simple example, without considering the direction 0 - 1 qr1 - qr0 @@ -265,7 +281,12 @@ def test_2q_circuit_5q_backend_max_trials(self): qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - tqc = transpile(circuit, backend, layout_method="dense") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) cmap = CouplingMap(backend.configuration().coupling_map) @@ -287,7 +308,7 @@ def test_2q_circuit_5q_backend_max_trials(self): self.assertLayout(dag, cmap, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): + def test_best_mapping_ghz_state_full_device_multiple_qregs(self): """Test best mappings with multiple registers""" backend = GenericBackendV2( num_qubits=5, @@ -634,6 +655,32 @@ def test_skip_3q_circuit_v2(self): ) def test_best_mapping_ghz_state_full_device_multiple_qregs(self): + """Test best mappings with multiple registers""" + backend = GenericBackendV2( + num_qubits=5, + coupling_map=YORKTOWN_CMAP, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + seed=8, + ) + qr_a = QuantumRegister(2) + qr_b = QuantumRegister(3) + qc = QuantumCircuit(qr_a, qr_b) + qc.h(qr_a[0]) + qc.cx(qr_a[0], qr_a[1]) + qc.cx(qr_a[0], qr_b[0]) + qc.cx(qr_a[0], qr_b[1]) + qc.cx(qr_a[0], qr_b[2]) + qc.measure_all() + tqc = transpile(qc, seed_transpiler=self.seed, layout_method="trivial") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.coupling_map) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_best_mapping_ghz_state_full_device_multiple_qregs_v1(self): """Test best mappings with multiple registers""" with self.assertWarns(DeprecationWarning): backend = Fake5QV1() @@ -646,7 +693,12 @@ def test_best_mapping_ghz_state_full_device_multiple_qregs(self): qc.cx(qr_a[0], qr_b[1]) qc.cx(qr_a[0], qr_b[2]) qc.measure_all() - tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") initial_layout = tqc._layout dag = circuit_to_dag(tqc) cmap = CouplingMap(backend.configuration().coupling_map) @@ -659,6 +711,29 @@ def test_best_mapping_ghz_state_full_device_multiple_qregs(self): self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_2q_circuit_5q_backend(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + backend = GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + seed=42, + ) + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.coupling_map) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend_v1(self): """A simple example, without considering the direction 0 - 1 qr1 - qr0 @@ -669,7 +744,12 @@ def test_2q_circuit_5q_backend(self): qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - tqc = transpile(circuit, backend, layout_method="dense") + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `transpile` function will " + "stop supporting inputs of type `BackendV1`", + ): + tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) cmap = CouplingMap(backend.configuration().coupling_map) diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index 1be7ad7ce051..bcf5b77d51bd 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -20,7 +20,7 @@ from qiskit.visualization import circuit_drawer from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile -from qiskit.providers.fake_provider import Fake5QV1 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit.library import ( XGate, MCXGate, @@ -36,6 +36,7 @@ from qiskit.quantum_info.random import random_unitary from qiskit.utils import optionals from .visualization import QiskitVisualizationTestCase +from ..legacy_cmaps import YORKTOWN_CMAP pi = np.pi @@ -481,8 +482,12 @@ def test_partial_layout(self): See: https://github.com/Qiskit/qiskit-terra/issues/4757""" filename = self._get_resource_path("test_latex_partial_layout.tex") - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() + backend = GenericBackendV2( + num_qubits=5, + coupling_map=YORKTOWN_CMAP, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + seed=42, + ) circuit = QuantumCircuit(3) circuit.h(1) diff --git a/test/utils/base.py b/test/utils/base.py index 6d0c917ace30..78d5aceb58f2 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -129,6 +129,15 @@ def setUpClass(cls): module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", ) + # Safe to remove once https://github.com/Qiskit/qiskit-aer/issues/2197 is in a release version + # of Aer. + warnings.filterwarnings( + "ignore", # If "default", it floods the CI output + category=DeprecationWarning, + message=r"The class ``qiskit\.providers\.models\..*`", + module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", + ) + # Safe to remove once https://github.com/Qiskit/qiskit-aer/issues/2065 is in a release version # of Aer. warnings.filterwarnings( From 4b056a7edf5c2d821753940336a3fac560de34d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:03:23 +0000 Subject: [PATCH 22/27] Bump bytemuck from 1.16.1 to 1.16.3 (#12865) Bumps [bytemuck](https://github.com/Lokathor/bytemuck) from 1.16.1 to 1.16.3. - [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md) - [Commits](https://github.com/Lokathor/bytemuck/compare/v1.16.1...v1.16.3) --- updated-dependencies: - dependency-name: bytemuck 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> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f91bab4d5603..022b46f9a2a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" dependencies = [ "bytemuck_derive", ] From f508ac783c8d9be2ed3c5927c6839551bcb451c1 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 31 Jul 2024 18:15:31 +0200 Subject: [PATCH 23/27] Fix annotated operation is modified during parameter assignment (#12869) --- qiskit/circuit/annotated_operation.py | 2 +- test/python/circuit/test_annotated_operation.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 6006e68f58df..c1093d8fc5bc 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -133,7 +133,7 @@ def __eq__(self, other) -> bool: def copy(self) -> "AnnotatedOperation": """Return a copy of the :class:`~.AnnotatedOperation`.""" - return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy()) + return AnnotatedOperation(base_op=self.base_op.copy(), modifiers=self.modifiers.copy()) def to_matrix(self): """Return a matrix representation (allowing to construct Operator).""" diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index e2ca9f4af9a4..375c19fcf3a6 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -204,6 +204,17 @@ def test_invalid_params_access(self): with self.assertRaises(AttributeError): _ = annotated.validate_parameter(1.2) + def test_invariant_under_assign(self): + """Test the annotated operation is not changed by assigning.""" + p = Parameter("p") + annotated = RXGate(p).control(2, annotated=True) + circuit = QuantumCircuit(annotated.num_qubits) + circuit.append(annotated, circuit.qubits) + bound = circuit.assign_parameters([1.23]) + + self.assertEqual(list(circuit.parameters), [p]) + self.assertEqual(len(bound.parameters), 0) + if __name__ == "__main__": unittest.main() From 919cfd3baf8e02c21ca26074e94191cedf98b5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:44:35 +0200 Subject: [PATCH 24/27] Revert base class change for Sampler and Estimator V1 implementations to avoid breaking instance checks. (#12871) --- qiskit/primitives/backend_estimator.py | 15 ++++++++------- qiskit/primitives/backend_sampler.py | 13 +++++++------ qiskit/primitives/estimator.py | 6 +++--- qiskit/primitives/sampler.py | 6 +++--- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index f6a53c02ea9e..722ee900ceea 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -36,7 +36,7 @@ ) from qiskit.utils.deprecation import deprecate_func -from .base import BaseEstimatorV1, EstimatorResult +from .base import BaseEstimator, EstimatorResult from .primitive_job import PrimitiveJob from .utils import _circuit_key, _observable_key, init_observable @@ -88,17 +88,18 @@ def _prepare_counts(results: list[Result]): return counts -class BackendEstimator(BaseEstimatorV1[PrimitiveJob[EstimatorResult]]): +class BackendEstimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): """Evaluates expectation value using Pauli rotation gates. The :class:`~.BackendEstimator` class is a generic implementation of the - :class:`~.BaseEstimatorV1` interface that is used to wrap a :class:`~.BackendV2` - (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimatorV1` API. It + :class:`~.BaseEstimator` (V1) interface that is used to wrap a :class:`~.BackendV2` + (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimator` V1 API. It facilitates using backends that do not provide a native - :class:`~.BaseEstimatorV1` implementation in places that work with - :class:`~.BaseEstimatorV1`. + :class:`~.BaseEstimator` V1 implementation in places that work with + :class:`~.BaseEstimator` V1. However, if you're using a provider that has a native implementation of - :class:`~.BaseEstimatorV1` or :class:`~.BaseEstimatorV2`, it is a better + :class:`~.BaseEstimatorV1` ( :class:`~.BaseEstimator`) or + :class:`~.BaseEstimatorV2`, it is a better choice to leverage that native implementation as it will likely include additional optimizations and be a more efficient implementation. The generic nature of this class precludes doing any provider- or diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 213b9701a556..905fa2371542 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -26,22 +26,23 @@ from qiskit.utils.deprecation import deprecate_func from .backend_estimator import _prepare_counts, _run_circuits -from .base import BaseSamplerV1, SamplerResult +from .base import BaseSampler, SamplerResult from .primitive_job import PrimitiveJob from .utils import _circuit_key -class BackendSampler(BaseSamplerV1[PrimitiveJob[SamplerResult]]): - """A :class:`~.BaseSamplerV1` implementation that provides a wrapper for +class BackendSampler(BaseSampler[PrimitiveJob[SamplerResult]]): + """A :class:`~.BaseSampler` (V1) implementation that provides a wrapper for leveraging the Sampler V1 interface from any backend. This class provides a sampler interface from any backend and doesn't do any measurement mitigation, it just computes the probability distribution from the counts. It facilitates using backends that do not provide a - native :class:`~.BaseSamplerV1` implementation in places that work with - :class:`~.BaseSamplerV1`. + native :class:`~.BaseSampler` V1 implementation in places that work with + :class:`~.BaseSampler` V1. However, if you're using a provider that has a native implementation of - :class:`~.BaseSamplerV1` or :class:`~.BaseESamplerV2`, it is a better + :class:`~.BaseSamplerV1` ( :class:`~.BaseSampler`) or + :class:`~.BaseESamplerV2`, it is a better choice to leverage that native implementation as it will likely include additional optimizations and be a more efficient implementation. The generic nature of this class precludes doing any provider- or diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 1ca1529852c3..6ae50ee7f0f5 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -26,7 +26,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func -from .base import BaseEstimatorV1, EstimatorResult +from .base import BaseEstimator, EstimatorResult from .primitive_job import PrimitiveJob from .utils import ( _circuit_key, @@ -36,9 +36,9 @@ ) -class Estimator(BaseEstimatorV1[PrimitiveJob[EstimatorResult]]): +class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): """ - Reference implementation of :class:`BaseEstimatorV1`. + Reference implementation of :class:`BaseEstimator` (V1). :Run Options: diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index d93db4f04116..9155e50ad2ce 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -26,7 +26,7 @@ from qiskit.result import QuasiDistribution from qiskit.utils.deprecation import deprecate_func -from .base import BaseSamplerV1, SamplerResult +from .base import BaseSampler, SamplerResult from .primitive_job import PrimitiveJob from .utils import ( _circuit_key, @@ -36,11 +36,11 @@ ) -class Sampler(BaseSamplerV1[PrimitiveJob[SamplerResult]]): +class Sampler(BaseSampler[PrimitiveJob[SamplerResult]]): """ Sampler V1 class. - :class:`~Sampler` is a reference implementation of :class:`~BaseSamplerV1`. + :class:`~Sampler` is a reference implementation of :class:`~BaseSampler` (V1). :Run Options: From 0afb06e8ac6d176443cee605849a563286fb1058 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jul 2024 19:50:16 +0100 Subject: [PATCH 25/27] Avoid operator creation in transpiler (#12826) * Avoid operator creation in transpiler This removes very nearly all of the use of `DAGOpNode.op` in the default transpiler paths. The sole exception is in `InverseCancellation`, which currently would involve some quite awkward gymnastics for little near-term benefit. The pass should move fully to Rust soon, making it not worth the effort. Most of the tricks here involve using the knowledge that most operations will involve only Rust-space standard gates, and that these cannot be control-flow operations. * Fix `HighLevelSynthesis` fast path --------- Co-authored-by: Matthew Treinish --- crates/circuit/src/circuit_instruction.rs | 32 +++++- crates/circuit/src/dag_node.rs | 27 ++++- crates/circuit/src/imports.rs | 2 + crates/circuit/src/operations.rs | 3 +- crates/circuit/src/packed_instruction.rs | 2 + qiskit/circuit/commutation_checker.py | 4 +- qiskit/dagcircuit/dagcircuit.py | 85 ++++++++------ .../passes/basis/basis_translator.py | 6 +- .../transpiler/passes/layout/apply_layout.py | 16 ++- qiskit/transpiler/passes/layout/vf2_utils.py | 6 +- .../transpiler/passes/routing/sabre_swap.py | 31 ++--- .../passes/synthesis/high_level_synthesis.py | 106 +++++++++++++----- qiskit/transpiler/passes/utils/check_map.py | 3 +- .../utils/convert_conditions_to_if_ops.py | 5 +- qiskit/transpiler/passes/utils/gates_basis.py | 3 +- .../transpiler/preset_passmanagers/common.py | 5 +- .../avoid-op-creation-804c0bed6c408911.yaml | 17 +++ 17 files changed, 241 insertions(+), 112 deletions(-) create mode 100644 releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 3ab0fe6279f7..d44051745a89 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -22,7 +22,9 @@ use pyo3::{intern, IntoPy, PyObject, PyResult}; use smallvec::SmallVec; -use crate::imports::{GATE, INSTRUCTION, OPERATION, WARNINGS_WARN}; +use crate::imports::{ + CONTROLLED_GATE, CONTROL_FLOW_OP, GATE, INSTRUCTION, OPERATION, WARNINGS_WARN, +}; use crate::operations::{ Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, }; @@ -266,11 +268,36 @@ impl CircuitInstruction { .and_then(|attrs| attrs.unit.as_deref()) } - #[getter] + /// Is the :class:`.Operation` contained in this instruction a Qiskit standard gate? pub fn is_standard_gate(&self) -> bool { self.operation.try_standard_gate().is_some() } + /// Is the :class:`.Operation` contained in this instruction a subclass of + /// :class:`.ControlledGate`? + pub fn is_controlled_gate(&self, py: Python) -> PyResult { + match self.operation.view() { + OperationRef::Standard(standard) => Ok(standard.num_ctrl_qubits() != 0), + OperationRef::Gate(gate) => gate + .gate + .bind(py) + .is_instance(CONTROLLED_GATE.get_bound(py)), + _ => Ok(false), + } + } + + /// Is the :class:`.Operation` contained in this node a directive? + pub fn is_directive(&self) -> bool { + self.op().directive() + } + + /// Is the :class:`.Operation` contained in this instruction a control-flow operation (i.e. an + /// instance of :class:`.ControlFlowOp`)? + pub fn is_control_flow(&self) -> bool { + self.op().control_flow() + } + + /// Does this instruction contain any :class:`.ParameterExpression` parameters? pub fn is_parameterized(&self) -> bool { self.params .iter() @@ -557,6 +584,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?, params: params.len() as u32, op_name: ob.getattr(intern!(py, "name"))?.extract()?, + control_flow: ob.is_instance(CONTROL_FLOW_OP.get_bound(py))?, instruction: ob.into_py(py), }); return Ok(OperationFromPython { diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index db9f6f650174..73983c35e2e8 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -291,10 +291,6 @@ impl DAGOpNode { self.instruction.params = val; } - pub fn is_parameterized(&self) -> bool { - self.instruction.is_parameterized() - } - #[getter] fn matrix(&self, py: Python) -> Option { let matrix = self.instruction.op().matrix(&self.instruction.params); @@ -333,11 +329,32 @@ impl DAGOpNode { .and_then(|attrs| attrs.unit.as_deref()) } - #[getter] + /// Is the :class:`.Operation` contained in this node a Qiskit standard gate? pub fn is_standard_gate(&self) -> bool { self.instruction.is_standard_gate() } + /// Is the :class:`.Operation` contained in this node a subclass of :class:`.ControlledGate`? + pub fn is_controlled_gate(&self, py: Python) -> PyResult { + self.instruction.is_controlled_gate(py) + } + + /// Is the :class:`.Operation` contained in this node a directive? + pub fn is_directive(&self) -> bool { + self.instruction.is_directive() + } + + /// Is the :class:`.Operation` contained in this node a control-flow operation (i.e. an instance + /// of :class:`.ControlFlowOp`)? + pub fn is_control_flow(&self) -> bool { + self.instruction.is_control_flow() + } + + /// Does this node contain any :class:`.ParameterExpression` parameters? + pub fn is_parameterized(&self) -> bool { + self.instruction.is_parameterized() + } + #[setter] fn set_label(&mut self, val: Option) { match self.instruction.extra_attrs.as_mut() { diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 47b29cc9c270..3a1c6ff5ff4a 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -61,6 +61,8 @@ pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.opera pub static INSTRUCTION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); pub static GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.gate", "Gate"); +pub static CONTROL_FLOW_OP: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp"); pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); pub static PARAMETER_EXPRESSION: ImportOnceCell = diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 8d3cfdf7f007..caf905a8c036 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -2016,6 +2016,7 @@ pub struct PyInstruction { pub clbits: u32, pub params: u32, pub op_name: String, + pub control_flow: bool, pub instruction: PyObject, } @@ -2033,7 +2034,7 @@ impl Operation for PyInstruction { self.params } fn control_flow(&self) -> bool { - false + self.control_flow } fn matrix(&self, _params: &[Param]) -> Option> { None diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index c909ca3d1b56..ac8795d664c1 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -283,6 +283,7 @@ impl PackedOperation { qubits: instruction.qubits, clbits: instruction.clbits, params: instruction.params, + control_flow: instruction.control_flow, op_name: instruction.op_name.clone(), } .into()), @@ -316,6 +317,7 @@ impl PackedOperation { qubits: instruction.qubits, clbits: instruction.clbits, params: instruction.params, + control_flow: instruction.control_flow, op_name: instruction.op_name.clone(), }) .into()), diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 5c1fb5586cb7..34cc66e9f1d6 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -66,11 +66,11 @@ def commute_nodes( """Checks if two DAGOpNodes commute.""" qargs1 = op1.qargs cargs1 = op2.cargs - if not op1.is_standard_gate: + if not op1.is_standard_gate(): op1 = op1.op qargs2 = op2.qargs cargs2 = op2.cargs - if not op2.is_standard_gate: + if not op2.is_standard_gate(): op2 = op2.op return self.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 28a1c16002fa..53cbc6f8f7f1 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -974,15 +974,26 @@ def _reject_new_register(reg): elif isinstance(nd, DAGOpNode): m_qargs = [edge_map.get(x, x) for x in nd.qargs] 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: - if not isinstance(op, ControlFlowOp): - op = op.c_if(*variable_mapper.map_condition(condition, allow_reorder=True)) + inst = nd._to_circuit_instruction(deepcopy=True) + m_op = None + if inst.condition is not None: + if inst.is_control_flow(): + m_op = inst.operation + m_op.condition = variable_mapper.map_condition( + inst.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) + m_op = inst.operation.c_if( + *variable_mapper.map_condition(inst.condition, allow_reorder=True) + ) + elif inst.is_control_flow() and isinstance(inst.operation, SwitchCaseOp): + m_op = inst.operation + m_op.target = variable_mapper.map_target(m_op.target) + if m_op is None: + inst = inst.replace(qubits=m_qargs, clbits=m_cargs) + else: + inst = inst.replace(operation=m_op, qubits=m_qargs, clbits=m_cargs) + dag._apply_op_node_back(DAGOpNode.from_instruction(inst), check=False) else: raise DAGCircuitError(f"bad node type {type(nd)}") @@ -1460,11 +1471,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit reverse_wire_map = {b: a for a, b in wire_map.items()} # It doesn't make sense to try and propagate a condition from a control-flow op; a # replacement for the control-flow op should implement the operation completely. - if ( - propagate_condition - and not isinstance(node.op, ControlFlowOp) - and (op_condition := getattr(node.op, "condition", None)) is not None - ): + if propagate_condition and not node.is_control_flow() and node.condition is not None: in_dag = input_dag.copy_empty_like() # The remapping of `condition` below is still using the old code that assumes a 2-tuple. # This is because this remapping code only makes sense in the case of non-control-flow @@ -1473,7 +1480,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit # in favour of the new-style conditional blocks. The extra logic in here to add # additional wires into the map as necessary would hugely complicate matters if we tried # to abstract it out into the `VariableMapper` used elsewhere. - target, value = op_condition + target, value = node.condition if isinstance(target, Clbit): new_target = reverse_wire_map.get(target, Clbit()) if new_target not in wire_map: @@ -1593,25 +1600,31 @@ def edge_weight_map(wire): for old_node_index, new_node_index in node_map.items(): # update node attributes old_node = in_dag._multi_graph[old_node_index] - if isinstance(old_node.op, SwitchCaseOp): + m_op = None + if not old_node.is_standard_gate() and isinstance(old_node.op, SwitchCaseOp): m_op = SwitchCaseOp( variable_mapper.map_target(old_node.op.target), old_node.op.cases_specifier(), label=old_node.op.label, ) - elif getattr(old_node.op, "condition", None) is not None: + elif old_node.condition is not None: m_op = old_node.op - if not isinstance(old_node.op, ControlFlowOp): + if old_node.is_control_flow(): + m_op.condition = variable_mapper.map_condition(m_op.condition) + else: 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] m_cargs = [wire_map[x] for x in old_node.cargs] - new_node = DAGOpNode(m_op, qargs=m_qargs, cargs=m_cargs, dag=self) + old_instruction = old_node._to_circuit_instruction() + if m_op is None: + new_instruction = old_instruction.replace(qubits=m_qargs, clbits=m_cargs) + else: + new_instruction = old_instruction.replace( + operation=m_op, qubits=m_qargs, clbits=m_cargs + ) + new_node = DAGOpNode.from_instruction(new_instruction) new_node._node_id = new_node_index self._multi_graph[new_node_index] = new_node self._increment_op(new_node.name) @@ -1840,11 +1853,18 @@ def op_nodes(self, op=None, include_directives=True): list[DAGOpNode]: the list of node ids containing the given op. """ nodes = [] + filter_is_nonstandard = getattr(op, "_standard_gate", None) is None for node in self._multi_graph.nodes(): if isinstance(node, DAGOpNode): - if not include_directives and getattr(node.op, "_directive", False): + if not include_directives and node.is_directive(): continue - if op is None or isinstance(node.op, op): + if op is None or ( + # This middle catch is to avoid Python-space operation creation for most uses of + # `op`; we're usually just looking for control-flow ops, and standard gates + # aren't control-flow ops. + not (filter_is_nonstandard and node.is_standard_gate()) + and isinstance(node.op, op) + ): nodes.append(node) return nodes @@ -1864,7 +1884,7 @@ def named_nodes(self, *names): """Get the set of "op" nodes with the given name.""" named_nodes = [] for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode) and node.op.name in names: + if isinstance(node, DAGOpNode) and node.name in names: named_nodes.append(node) return named_nodes @@ -2070,14 +2090,11 @@ def layers(self, *, vars_mode: _VarsMode = "captures"): new_layer = self.copy_empty_like(vars_mode=vars_mode) for node in op_nodes: - # this creates new DAGOpNodes in the new_layer - new_layer.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + new_layer._apply_op_node_back(node, check=False) # The quantum registers that have an operation in this layer. support_list = [ - op_node.qargs - for op_node in new_layer.op_nodes() - if not getattr(op_node.op, "_directive", False) + op_node.qargs for op_node in new_layer.op_nodes() if not op_node.is_directive() ] yield {"graph": new_layer, "partition": support_list} @@ -2129,11 +2146,7 @@ def collect_runs(self, namelist): """ def filter_fn(node): - return ( - isinstance(node, DAGOpNode) - and node.op.name in namelist - and getattr(node.op, "condition", None) is None - ) + return isinstance(node, DAGOpNode) and node.name in namelist and node.condition is None group_list = rx.collect_runs(self._multi_graph, filter_fn) return {tuple(x) for x in group_list} @@ -2366,7 +2379,7 @@ def _may_have_additional_wires(node) -> bool: # # If updating this, you most likely also need to update `_additional_wires`. return node.condition is not None or ( - not node.is_standard_gate and isinstance(node.op, (ControlFlowOp, Store)) + not node.is_standard_gate() and isinstance(node.op, (ControlFlowOp, Store)) ) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 8bf98f38e087..0d597b89de6a 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -323,7 +323,7 @@ def _replace_node(self, dag, node, instr_map): node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs ) - if not new_node.is_standard_gate: + if not new_node.is_standard_gate(): new_node.op = new_node.op.copy() if any(isinstance(x, ParameterExpression) for x in inner_node.params): new_params = [] @@ -342,7 +342,7 @@ def _replace_node(self, dag, node, instr_map): new_value = new_value.numeric() new_params.append(new_value) new_node.params = new_params - if not new_node.is_standard_gate: + if not new_node.is_standard_gate(): new_node.op.params = new_params dag._apply_op_node_back(new_node) @@ -533,7 +533,7 @@ def edge_cost(self, edge_data): cost_tot = 0 for instruction in edge_data.rule.circuit: - key = Key(name=instruction.operation.name, num_qubits=len(instruction.qubits)) + key = Key(name=instruction.name, num_qubits=len(instruction.qubits)) cost_tot += self._opt_cost_map[key] return cost_tot - self._opt_cost_map[edge_data.source] diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 9cbedcef5ab3..629dc32061fc 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -13,7 +13,7 @@ """Transform a circuit with virtual qubits into a circuit with physical qubits.""" from qiskit.circuit import QuantumRegister -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout @@ -79,7 +79,12 @@ def run(self, dag): virtual_physical_map = layout.get_virtual_bits() for node in dag.topological_op_nodes(): qargs = [q[virtual_physical_map[qarg]] for qarg in node.qargs] - new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False) + new_dag._apply_op_node_back( + DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qargs) + ), + check=False, + ) else: # First build a new layout object going from: # old virtual -> old physical -> new virtual -> new physical @@ -99,7 +104,12 @@ def run(self, dag): # Apply new layout to the circuit for node in dag.topological_op_nodes(): qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs] - new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False) + new_dag._apply_op_node_back( + DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qargs) + ), + check=False, + ) self.property_set["layout"] = full_layout if (final_layout := self.property_set["final_layout"]) is not None: final_layout_mapping = { diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index c5d420127f88..037ccc37155d 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -19,7 +19,7 @@ import numpy as np from rustworkx import PyDiGraph, PyGraph, connected_components -from qiskit.circuit import ControlFlowOp, ForLoopOp +from qiskit.circuit import ForLoopOp from qiskit.converters import circuit_to_dag from qiskit._accelerate import vf2_layout from qiskit._accelerate.nlayout import NLayout @@ -37,7 +37,7 @@ class MultiQEncountered(Exception): def _visit(dag, weight, wire_map): for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.is_control_flow(): if isinstance(node.op, ForLoopOp): inner_weight = len(node.op.params[0]) * weight else: @@ -57,7 +57,7 @@ def _visit(dag, weight, wire_map): im_graph_node_map[qargs[0]] = im_graph.add_node(weights) reverse_im_graph_node_map[im_graph_node_map[qargs[0]]] = qargs[0] else: - im_graph[im_graph_node_map[qargs[0]]][node.op.name] += weight + im_graph[im_graph_node_map[qargs[0]]][node.name] += weight if len_args == 2: if qargs[0] not in im_graph_node_map: im_graph_node_map[qargs[0]] = im_graph.add_node(defaultdict(int)) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index c8efaabea2bf..788f0d995754 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -18,7 +18,7 @@ import rustworkx -from qiskit.circuit import SwitchCaseOp, ControlFlowOp, Clbit, ClassicalRegister +from qiskit.circuit import SwitchCaseOp, Clbit, ClassicalRegister from qiskit.circuit.library.standard_gates import SwapGate from qiskit.circuit.controlflow import condition_resources, node_resources from qiskit.converters import dag_to_circuit @@ -28,7 +28,7 @@ from qiskit.transpiler.layout import Layout from qiskit.transpiler.target import Target from qiskit.transpiler.passes.layout import disjoint_utils -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.utils.parallel import CPU_COUNT from qiskit._accelerate.sabre import sabre_routing, Heuristic, SetScaling, NeighborTable, SabreDAG @@ -297,9 +297,9 @@ def process_dag(block_dag, wire_map): node_blocks = {} for node in block_dag.topological_op_nodes(): cargs_bits = set(node.cargs) - if node.op.condition is not None: - cargs_bits.update(condition_resources(node.op.condition).clbits) - if isinstance(node.op, SwitchCaseOp): + if node.condition is not None: + cargs_bits.update(condition_resources(node.condition).clbits) + if node.is_control_flow() and isinstance(node.op, SwitchCaseOp): target = node.op.target if isinstance(target, Clbit): cargs_bits.add(target) @@ -308,7 +308,7 @@ def process_dag(block_dag, wire_map): else: # Expr cargs_bits.update(node_resources(target).clbits) cargs = {block_dag.find_bit(x).index for x in cargs_bits} - if isinstance(node.op, ControlFlowOp): + if node.is_control_flow(): node_blocks[node._node_id] = [ recurse( block, @@ -321,7 +321,7 @@ def process_dag(block_dag, wire_map): node._node_id, [wire_map[x] for x in node.qargs], cargs, - getattr(node.op, "_directive", False), + node.is_directive(), ) ) return SabreDAG(num_physical_qubits, block_dag.num_clbits(), dag_list, node_blocks) @@ -391,14 +391,15 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): node = source_dag._multi_graph[node_id] if node_id in swap_map: apply_swaps(dest_dag, swap_map[node_id], layout) - if not isinstance(node.op, ControlFlowOp): - dest_dag.apply_operation_back( - node.op, - [ - physical_qubits[layout.virtual_to_physical(root_logical_map[q])] - for q in node.qargs - ], - node.cargs, + if not node.is_control_flow(): + qubits = [ + physical_qubits[layout.virtual_to_physical(root_logical_map[q])] + for q in node.qargs + ] + dest_dag._apply_op_node_back( + DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qubits) + ), check=False, ) continue diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index f1de2b8f2136..6974b1cce06c 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -159,7 +159,10 @@ QFTSynthesisLine """ -from typing import Optional, Union, List, Tuple, Callable +from __future__ import annotations + +import typing +from typing import Optional, Union, List, Tuple, Callable, Sequence import numpy as np import rustworkx as rx @@ -168,7 +171,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit import ControlFlowOp, ControlledGate, EquivalenceLibrary +from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence from qiskit.circuit.library import LinearFunction from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target @@ -210,6 +213,9 @@ from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin +if typing.TYPE_CHECKING: + from qiskit.dagcircuit import DAGOpNode + class HLSConfig: """The high-level-synthesis config allows to specify a list of "methods" used by @@ -396,6 +402,8 @@ def __init__( if not self._top_level_only and (self._target is None or self._target.num_qubits is None): basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) + else: + self._device_insts = set() def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the HighLevelSynthesis pass on `dag`. @@ -415,11 +423,11 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: dag_op_nodes = dag.op_nodes() for node in dag_op_nodes: - if isinstance(node.op, ControlFlowOp): + if node.is_control_flow(): node.op = control_flow.map_blocks(self.run, node.op) continue - if getattr(node.op, "_directive", False): + if node.is_directive(): continue if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: @@ -429,6 +437,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None ) + if self._definitely_skip_node(node, qubits): + continue + decomposition, modified = self._recursively_handle_op(node.op, qubits) if not modified: @@ -445,6 +456,43 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return dag + def _definitely_skip_node(self, node: DAGOpNode, qubits: Sequence[int] | None) -> bool: + """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will + attempt to synthesise it) without accessing its Python-space `Operation`. + + This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to + avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the + node (which is _most_ nodes).""" + return ( + # The fast path is just for Rust-space standard gates (which excludes + # `AnnotatedOperation`). + node.is_standard_gate() + # If it's a controlled gate, we might choose to do funny things to it. + and not node.is_controlled_gate() + # If there are plugins to try, they need to be tried. + and not self._methods_to_try(node.name) + # If all the above constraints hold, and it's already supported or the basis translator + # can handle it, we'll leave it be. + and ( + self._instruction_supported(node.name, qubits) + # This uses unfortunately private details of `EquivalenceLibrary`, but so does the + # `BasisTranslator`, and this is supposed to just be temporary til this is moved + # into Rust space. + or ( + self._equiv_lib is not None + and equivalence.Key(name=node.name, num_qubits=node.num_qubits) + in self._equiv_lib._key_to_node_index + ) + ) + ) + + def _instruction_supported(self, name: str, qubits: Sequence[int]) -> bool: + qubits = tuple(qubits) if qubits is not None else None + # include path for when target exists but target.num_qubits is None (BasicSimulator) + if self._target is None or self._target.num_qubits is None: + return name in self._device_insts + return self._target.instruction_supported(operation_name=name, qargs=qubits) + def _recursively_handle_op( self, op: Operation, qubits: Optional[List] = None ) -> Tuple[Union[QuantumCircuit, DAGCircuit, Operation], bool]: @@ -472,6 +520,9 @@ def _recursively_handle_op( an annotated operation. """ + # WARNING: if adding new things in here, ensure that `_definitely_skip_node` is also + # up-to-date. + # Try to apply plugin mechanism decomposition = self._synthesize_op_using_plugins(op, qubits) if decomposition is not None: @@ -490,17 +541,9 @@ def _recursively_handle_op( # or is in equivalence library controlled_gate_open_ctrl = isinstance(op, ControlledGate) and op._open_ctrl if not controlled_gate_open_ctrl: - qargs = tuple(qubits) if qubits is not None else None - # include path for when target exists but target.num_qubits is None (BasicSimulator) - inst_supported = ( - self._target.instruction_supported( - operation_name=op.name, - qargs=qargs, - ) - if self._target is not None and self._target.num_qubits is not None - else op.name in self._device_insts - ) - if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)): + if self._instruction_supported(op.name, qubits) or ( + self._equiv_lib is not None and self._equiv_lib.has_entry(op) + ): return op, False try: @@ -521,6 +564,22 @@ def _recursively_handle_op( dag = self.run(dag) return dag, True + def _methods_to_try(self, name: str): + """Get a sequence of methods to try for a given op name.""" + if (methods := self.hls_config.methods.get(name)) is not None: + # the operation's name appears in the user-provided config, + # we use the list of methods provided by the user + return methods + if ( + self.hls_config.use_default_on_unspecified + and "default" in self.hls_plugin_manager.method_names(name) + ): + # the operation's name does not appear in the user-specified config, + # we use the "default" method when instructed to do so and the "default" + # method is available + return ["default"] + return [] + def _synthesize_op_using_plugins( self, op: Operation, qubits: List ) -> Union[QuantumCircuit, None]: @@ -531,25 +590,10 @@ def _synthesize_op_using_plugins( """ hls_plugin_manager = self.hls_plugin_manager - if op.name in self.hls_config.methods.keys(): - # the operation's name appears in the user-provided config, - # we use the list of methods provided by the user - methods = self.hls_config.methods[op.name] - elif ( - self.hls_config.use_default_on_unspecified - and "default" in hls_plugin_manager.method_names(op.name) - ): - # the operation's name does not appear in the user-specified config, - # we use the "default" method when instructed to do so and the "default" - # method is available - methods = ["default"] - else: - methods = [] - best_decomposition = None best_score = np.inf - for method in methods: + for method in self._methods_to_try(op.name): # There are two ways to specify a synthesis method. The more explicit # way is to specify it as a tuple consisting of a synthesis algorithm and a # list of additional arguments, e.g., diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index 437718ec27b4..bd78c65de5f4 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -14,7 +14,6 @@ from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.target import Target -from qiskit.circuit.controlflow import ControlFlowOp from qiskit.converters import circuit_to_dag @@ -73,7 +72,7 @@ def run(self, dag): def _recurse(self, dag, wire_map) -> bool: for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.is_control_flow(): for block in node.op.blocks: inner_wire_map = { inner: wire_map[outer] for inner, outer in zip(block.qubits, node.qargs) diff --git a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py index 46d91f7d6ce0..a73f9690ae0e 100644 --- a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py +++ b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py @@ -17,7 +17,6 @@ CircuitInstruction, ClassicalRegister, Clbit, - ControlFlowOp, IfElseOp, QuantumCircuit, ) @@ -37,7 +36,7 @@ def _run_inner(self, dag): was modified and ``False`` if not.""" modified = False for node in dag.op_nodes(): - if isinstance(node.op, ControlFlowOp): + if node.is_control_flow(): modified_blocks = False new_dags = [] for block in node.op.blocks: @@ -51,7 +50,7 @@ def _run_inner(self, dag): node.op.replace_blocks(dag_to_circuit(block) for block in new_dags), inplace=True, ) - elif getattr(node.op, "condition", None) is None: + elif node.condition is None: continue else: target, value = node.op.condition diff --git a/qiskit/transpiler/passes/utils/gates_basis.py b/qiskit/transpiler/passes/utils/gates_basis.py index b1f004cc0df3..16a68c3e533e 100644 --- a/qiskit/transpiler/passes/utils/gates_basis.py +++ b/qiskit/transpiler/passes/utils/gates_basis.py @@ -12,7 +12,6 @@ """Check if all gates in the DAGCircuit are in the specified basis gates.""" -from qiskit.circuit import ControlFlowOp from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import AnalysisPass @@ -55,7 +54,7 @@ def _visit_target(dag, wire_map): return True # Control-flow ops still need to be supported, so don't skip them in the # previous checks. - if isinstance(gate.op, ControlFlowOp): + if gate.is_control_flow(): for block in gate.op.blocks: inner_wire_map = { inner: wire_map[outer] diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index ec479f9e006b..b0e9eae57b03 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -364,10 +364,7 @@ def _swap_condition(property_set): routing.append(ConditionalController(ApplyLayout(), condition=_apply_post_layout_condition)) def filter_fn(node): - return ( - getattr(node.op, "label", None) - != "qiskit.transpiler.internal.routing.protection.barrier" - ) + return node.label != "qiskit.transpiler.internal.routing.protection.barrier" routing.append([FilterOpNodes(filter_fn)]) diff --git a/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml b/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml new file mode 100644 index 000000000000..c7f63a45f8cc --- /dev/null +++ b/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + :class:`.CircuitInstruction` and :class:`.DAGOpNode` each have new methods to query various + properties of their internal :class:`.Operation`, without necessarily needing to access it. + These methods are: + + * :meth:`.CircuitInstruction.is_standard_gate` and :meth:`.DAGOpNode.is_standard_gate`, + * :meth:`.CircuitInstruction.is_controlled_gate` and :meth:`.DAGOpNode.is_controlled_gate`, + * :meth:`.CircuitInstruction.is_directive` and :meth:`.DAGOpNode.is_directive`, + * :meth:`.CircuitInstruction.is_control_flow` and :meth:`.DAGOpNode.is_control_flow`, and + * :meth:`.CircuitInstruction.is_parameterized` and :meth:`.DAGOpNode.is_parameterized`. + + If applicable, using any of these methods is significantly faster than querying + :attr:`.CircuitInstruction.operation` or :attr:`.DAGOpNode.op` directly, especially if the + instruction or node represents a Qiskit standard gate. This is because the standard gates are + stored natively in Rust, and their Python representation is only created when requested. From 441925e4cf16f058a927caf9145a599ac6782b41 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 1 Aug 2024 11:23:26 +0200 Subject: [PATCH 26/27] Speedup the `CommutationChecker` and the `CommutativeCancellation` (#12859) * Faster commutation checker and analysis - list of pre-approved gates we know we support commutation on - less redirections in function calls - commutation analysis to only trigger search on gates that are actually cancelled * cleanup comments * add reno * review comments * revert accidentially changed tests -- these need updating only on the version with parameter support * revert changes in test_comm_inv_canc --------- Co-authored-by: MarcDrudis --- qiskit/circuit/commutation_checker.py | 98 +++++++++++-------- qiskit/circuit/instruction.py | 2 +- .../optimization/commutation_analysis.py | 8 +- .../optimization/commutative_cancellation.py | 26 +++-- 4 files changed, 83 insertions(+), 51 deletions(-) diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 34cc66e9f1d6..e070fe9b9b6f 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -13,7 +13,7 @@ """Code from commutative_analysis pass that checks commutation relations between DAG nodes.""" from functools import lru_cache -from typing import List, Union +from typing import List, Union, Set, Optional import numpy as np from qiskit import QiskitError @@ -25,6 +25,27 @@ _skipped_op_names = {"measure", "reset", "delay", "initialize"} _no_cache_op_names = {"annotated"} +_supported_ops = { + "h", + "x", + "y", + "z", + "sx", + "sxdg", + "t", + "tdg", + "s", + "sdg", + "cx", + "cy", + "cz", + "swap", + "iswap", + "ecr", + "ccx", + "cswap", +} + @lru_cache(maxsize=None) def _identity_op(num_qubits): @@ -42,7 +63,13 @@ class CommutationChecker: evicting from the cache less useful entries, etc. """ - def __init__(self, standard_gate_commutations: dict = None, cache_max_entries: int = 10**6): + def __init__( + self, + standard_gate_commutations: dict = None, + cache_max_entries: int = 10**6, + *, + gates: Optional[Set[str]] = None, + ): super().__init__() if standard_gate_commutations is None: self._standard_commutations = {} @@ -56,6 +83,7 @@ def __init__(self, standard_gate_commutations: dict = None, cache_max_entries: i self._current_cache_entries = 0 self._cache_miss = 0 self._cache_hit = 0 + self._gate_names = gates def commute_nodes( self, @@ -103,6 +131,11 @@ def commute( Returns: bool: whether two operations commute. """ + # Skip gates that are not specified. + if self._gate_names is not None: + if op1.name not in self._gate_names or op2.name not in self._gate_names: + return False + structural_commutation = _commutation_precheck( op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits ) @@ -231,59 +264,38 @@ def _hashable_parameters(params): return ("fallback", str(params)) -def is_commutation_supported(op): +def is_commutation_supported(op, qargs, max_num_qubits): """ Filter operations whose commutation is not supported due to bugs in transpiler passes invoking commutation analysis. Args: - op (Operation): operation to be checked for commutation relation + op (Operation): operation to be checked for commutation relation. + qargs (list[Qubit]): qubits the operation acts on. + max_num_qubits (int): The maximum number of qubits to check commutativity for. + Return: True if determining the commutation of op is currently supported """ - # Bug in CommutativeCancellation, e.g. see gh-8553 - if getattr(op, "condition", False): + # If the number of qubits is beyond what we check, stop here and do not even check in the + # pre-defined supported operations + if len(qargs) > max_num_qubits: return False + # Check if the operation is pre-approved, otherwise go through the checks + if op.name in _supported_ops: + return True + # Commutation of ControlFlow gates also not supported yet. This may be pending a control flow graph. if op.name in CONTROL_FLOW_OP_NAMES: return False - return True - - -def is_commutation_skipped(op, qargs, max_num_qubits): - """ - Filter operations whose commutation will not be determined. - Args: - op (Operation): operation to be checked for commutation relation - qargs (List): operation qubits - max_num_qubits (int): the maximum number of qubits to consider, the check may be skipped if - the number of qubits for either operation exceeds this amount. - Return: - True if determining the commutation of op is currently not supported - """ - if ( - len(qargs) > max_num_qubits - or getattr(op, "_directive", False) - or op.name in _skipped_op_names - ): - return True + if getattr(op, "_directive", False) or op.name in _skipped_op_names: + return False if getattr(op, "is_parameterized", False) and op.is_parameterized(): - return True - - from qiskit.dagcircuit.dagnode import DAGOpNode - - # we can proceed if op has defined: to_operator, to_matrix and __array__, or if its definition can be - # recursively resolved by operations that have a matrix. We check this by constructing an Operator. - if ( - isinstance(op, DAGOpNode) - or (hasattr(op, "to_matrix") and hasattr(op, "__array__")) - or hasattr(op, "to_operator") - ): return False - return False + return True def _commutation_precheck( @@ -295,13 +307,14 @@ def _commutation_precheck( cargs2: List, max_num_qubits, ): - if not is_commutation_supported(op1) or not is_commutation_supported(op2): + # Bug in CommutativeCancellation, e.g. see gh-8553 + if getattr(op1, "condition", False) or getattr(op2, "condition", False): return False if set(qargs1).isdisjoint(qargs2) and set(cargs1).isdisjoint(cargs2): return True - if is_commutation_skipped(op1, qargs1, max_num_qubits) or is_commutation_skipped( + if not is_commutation_supported(op1, qargs1, max_num_qubits) or not is_commutation_supported( op2, qargs2, max_num_qubits ): return False @@ -409,7 +422,10 @@ def _query_commutation( first_params = getattr(first_op, "params", []) second_params = getattr(second_op, "params", []) return commutation_after_placement.get( - (_hashable_parameters(first_params), _hashable_parameters(second_params)), + ( + _hashable_parameters(first_params), + _hashable_parameters(second_params), + ), None, ) else: diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index d67fe4b5f12f..557d7df21ee1 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -302,7 +302,7 @@ def is_parameterized(self): """Return whether the :class:`Instruction` contains :ref:`compile-time parameters `.""" return any( - isinstance(param, ParameterExpression) and param.parameters for param in self.params + isinstance(param, ParameterExpression) and param.parameters for param in self._params ) @property diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 61c77de552b9..12ed7145eec7 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -27,9 +27,13 @@ class CommutationAnalysis(AnalysisPass): are grouped into a set of gates that commute. """ - def __init__(self): + def __init__(self, *, _commutation_checker=None): super().__init__() - self.comm_checker = scc + # allow setting a private commutation checker, this allows better performance if we + # do not care about commutations of all gates, but just a subset + if _commutation_checker is None: + _commutation_checker = scc + self.comm_checker = _commutation_checker def run(self, dag): """Run the CommutationAnalysis pass on `dag`. diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 5c0b7317aabd..adfc4d73a221 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -16,10 +16,12 @@ import numpy as np from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passes.optimization.commutation_analysis import CommutationAnalysis from qiskit.dagcircuit import DAGCircuit, DAGInNode, DAGOutNode +from qiskit.circuit.commutation_library import CommutationChecker, StandardGateCommutations from qiskit.circuit.library.standard_gates.u1 import U1Gate from qiskit.circuit.library.standard_gates.rx import RXGate from qiskit.circuit.library.standard_gates.p import PhaseGate @@ -61,7 +63,18 @@ def __init__(self, basis_gates=None, target=None): self.basis = set(target.operation_names) self._var_z_map = {"rz": RZGate, "p": PhaseGate, "u1": U1Gate} - self.requires.append(CommutationAnalysis()) + + self._z_rotations = {"p", "z", "u1", "rz", "t", "s"} + self._x_rotations = {"x", "rx"} + self._gates = {"cx", "cy", "cz", "h", "y"} # Now the gates supported are hard-coded + + # build a commutation checker restricted to the gates we cancel -- the others we + # do not have to investigate, which allows to save time + commutation_checker = CommutationChecker( + StandardGateCommutations, gates=self._gates | self._z_rotations | self._x_rotations + ) + + self.requires.append(CommutationAnalysis(_commutation_checker=commutation_checker)) def run(self, dag): """Run the CommutativeCancellation pass on `dag`. @@ -82,9 +95,6 @@ def run(self, dag): if z_var_gates: var_z_gate = self._var_z_map[next(iter(z_var_gates))] - # Now the gates supported are hard-coded - q_gate_list = ["cx", "cy", "cz", "h", "y"] - # Gate sets to be cancelled cancellation_sets = defaultdict(lambda: []) @@ -103,9 +113,11 @@ def run(self, dag): continue for node in com_set: num_qargs = len(node.qargs) - if num_qargs == 1 and node.name in q_gate_list: + if any(isinstance(p, ParameterExpression) for p in node.params): + continue # no support for cancellation of parameterized gates + if num_qargs == 1 and node.name in self._gates: cancellation_sets[(node.name, wire, com_set_idx)].append(node) - if num_qargs == 1 and node.name in ["p", "z", "u1", "rz", "t", "s"]: + if num_qargs == 1 and node.name in self._z_rotations: cancellation_sets[("z_rotation", wire, com_set_idx)].append(node) if num_qargs == 1 and node.name in ["rx", "x"]: cancellation_sets[("x_rotation", wire, com_set_idx)].append(node) @@ -126,7 +138,7 @@ def run(self, dag): if cancel_set_key[0] == "z_rotation" and var_z_gate is None: continue set_len = len(cancellation_sets[cancel_set_key]) - if set_len > 1 and cancel_set_key[0] in q_gate_list: + if set_len > 1 and cancel_set_key[0] in self._gates: gates_to_cancel = cancellation_sets[cancel_set_key] for c_node in gates_to_cancel[: (set_len // 2) * 2]: dag.remove_op_node(c_node) From a68de4f9ff4bcf1716d29d21b39db42ba55b1aca Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 1 Aug 2024 11:56:43 +0100 Subject: [PATCH 27/27] Move `QuantumCircuit.assign_parameters` to Rust (#12794) * Move `QuantumCircuit.assign_parameters` to Rust This is (as far as I could tell), the last really major performance regression in our asv suite compared to 1.1.0, so with this commit, we should be at _not worse_ for important utility-scale benchmarks. This largely rewrites `ParamTable` (renamed back to `ParameterTable` because I kept getting confused with `Param`) to have more Rust-friendly interfaces available, so that `assign_parameters` can then use them. This represents a 2-3x speedup in `assign_parameters` performance over 1.1.0, when binding simple `Parameter` instances. Approximately 75% of the time is now spent in Python-space `Parameter.assign` and `ParameterExpression.numeric` calls; almost all of this could be removed were we to move `Parameter` and `ParameterExpression` to have their data exposed directly to Rust space. The percentage of time spent in Python space only increases if the expressions to be bound are actual `ParameterExpression`s and not just `Parameter`. Most changes in the test suite are because of the use of internal-only methods that changed with the new `ParameterTable`. The only discrepancy is a bug in `test_pauli_feature_map`, which was trying to assign using a set. * Add unit test of parameter insertion This catches a bug that was present in the parent commit, but this PR fixes. * Update crates/circuit/src/imports.rs * Fix assignment to `AnnotatedOperation` * Rename `CircuitData::num_params` to match normal terminology * Fix typos and :us: * Fix lint --- crates/circuit/src/circuit_data.rs | 676 +++++++++++------- crates/circuit/src/imports.rs | 3 + crates/circuit/src/operations.rs | 66 +- crates/circuit/src/parameter_table.rs | 451 +++++++++--- qiskit/circuit/_utils.py | 13 - qiskit/circuit/quantumcircuit.py | 164 +---- qiskit/qasm3/exporter.py | 4 +- .../circuit/library/test_blueprintcircuit.py | 6 +- .../circuit/library/test_pauli_feature_map.py | 6 +- test/python/circuit/test_circuit_data.py | 4 +- .../python/circuit/test_circuit_operations.py | 2 +- test/python/circuit/test_parameters.py | 105 ++- 12 files changed, 921 insertions(+), 579 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index a325ca4e1d5c..a6b1ef84a437 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -15,22 +15,26 @@ use std::cell::RefCell; use crate::bit_data::BitData; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; -use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::imports::{ANNOTATED_OPERATION, QUANTUM_CIRCUIT, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; -use crate::operations::{Operation, Param, StandardGate}; +use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::PackedInstruction; -use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; +use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; use crate::slice::{PySequenceIndex, SequenceIndex}; use crate::{Clbit, Qubit}; -use pyo3::exceptions::{PyIndexError, PyValueError}; +use numpy::PyReadonlyArray1; +use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySet, PyTuple, PyType}; -use pyo3::{intern, PyTraverseError, PyVisit}; +use pyo3::pybacked::PyBackedStr; +use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyTuple, PyType}; +use pyo3::{import_exception, intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; use smallvec::SmallVec; +import_exception!(qiskit.circuit.exceptions, CircuitError); + /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning /// their :attr:`~.CircuitInstruction.qubits` and @@ -94,7 +98,7 @@ pub struct CircuitData { qubits: BitData, /// Clbits registered in the circuit. clbits: BitData, - param_table: ParamTable, + param_table: ParameterTable, #[pyo3(get)] global_phase: Param, } @@ -133,7 +137,7 @@ impl CircuitData { cargs_interner: IndexedInterner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), - param_table: ParamTable::new(), + param_table: ParameterTable::new(), global_phase, }; if num_qubits > 0 { @@ -160,172 +164,73 @@ impl CircuitData { #[cfg(feature = "cache_pygates")] py_op: RefCell::new(None), }); + res.track_instruction_parameters(py, res.data.len() - 1)?; } Ok(res) } - fn handle_manual_params( + /// Add the entries from the `PackedInstruction` at the given index to the internal parameter + /// table. + fn track_instruction_parameters( &mut self, py: Python, - inst_index: usize, - params: &[(usize, Vec)], - ) -> PyResult { - let mut new_param = false; - let mut atomic_parameters: HashMap = HashMap::new(); - for (param_index, raw_param_objs) in params { - raw_param_objs.iter().for_each(|x| { - atomic_parameters.insert( - x.getattr(py, intern!(py, "_uuid")) - .expect("Not a parameter") - .getattr(py, intern!(py, "int")) - .expect("Not a uuid") - .extract::(py) - .unwrap(), - x.clone_ref(py), - ); - }); - for (param_uuid, param_obj) in atomic_parameters.iter() { - match self.param_table.table.get_mut(param_uuid) { - Some(entry) => entry.add(inst_index, *param_index), - None => { - new_param = true; - let new_entry = ParamEntry::new(inst_index, *param_index); - self.param_table - .insert(py, param_obj.clone_ref(py), new_entry)?; - } - }; + instruction_index: usize, + ) -> PyResult<()> { + for (index, param) in self.data[instruction_index] + .params_view() + .iter() + .enumerate() + { + let usage = ParameterUse::Index { + instruction: instruction_index, + parameter: index as u32, + }; + for param_ob in param.iter_parameters(py)? { + self.param_table.track(¶m_ob?, Some(usage))?; } - atomic_parameters.clear() } - Ok(new_param) + Ok(()) } - /// Add an instruction's entries to the parameter table - fn update_param_table( + /// Remove the entries from the `PackedInstruction` at the given index from the internal + /// parameter table. + fn untrack_instruction_parameters( &mut self, py: Python, - inst_index: usize, - params: Option)>>, - ) -> PyResult { - if let Some(params) = params { - return self.handle_manual_params(py, inst_index, ¶ms); - } - // Update the parameter table - let mut new_param = false; - let inst_params = self.data[inst_index].params_view(); - if !inst_params.is_empty() { - let params: Vec<(usize, PyObject)> = inst_params - .iter() - .enumerate() - .filter_map(|(idx, x)| match x { - Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), - _ => None, - }) - .collect(); - if !params.is_empty() { - let list_builtin = BUILTIN_LIST.get_bound(py); - let mut atomic_parameters: HashMap = HashMap::new(); - for (param_index, param) in ¶ms { - let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; - let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; - raw_param_objs.iter().for_each(|x| { - atomic_parameters.insert( - x.getattr(py, intern!(py, "_uuid")) - .expect("Not a parameter") - .getattr(py, intern!(py, "int")) - .expect("Not a uuid") - .extract(py) - .unwrap(), - x.clone_ref(py), - ); - }); - for (param_uuid, param_obj) in &atomic_parameters { - match self.param_table.table.get_mut(param_uuid) { - Some(entry) => entry.add(inst_index, *param_index), - None => { - new_param = true; - let new_entry = ParamEntry::new(inst_index, *param_index); - self.param_table - .insert(py, param_obj.clone_ref(py), new_entry)?; - } - }; - } - atomic_parameters.clear(); - } - } - } - Ok(new_param) - } - - /// Remove an index's entries from the parameter table. - fn remove_from_parameter_table(&mut self, py: Python, inst_index: usize) -> PyResult<()> { - let list_builtin = BUILTIN_LIST.get_bound(py); - if inst_index == GLOBAL_PHASE_INDEX { - if let Param::ParameterExpression(global_phase) = &self.global_phase { - let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; - let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; - for (param_index, param_obj) in raw_param_objs.iter().enumerate() { - let uuid: u128 = param_obj - .getattr(py, intern!(py, "_uuid"))? - .getattr(py, intern!(py, "int"))? - .extract(py)?; - let name: String = param_obj.getattr(py, intern!(py, "name"))?.extract(py)?; - self.param_table - .discard_references(uuid, inst_index, param_index, name); - } - } - } else if !self.data[inst_index].params_view().is_empty() { - let params: Vec<(usize, PyObject)> = self.data[inst_index] - .params_view() - .iter() - .enumerate() - .filter_map(|(idx, x)| match x { - Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), - _ => None, - }) - .collect(); - if !params.is_empty() { - for (param_index, param) in ¶ms { - let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; - let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; - let mut atomic_parameters: HashSet<(u128, String)> = - HashSet::with_capacity(params.len()); - for x in raw_param_objs { - let uuid = x - .getattr(py, intern!(py, "_uuid"))? - .getattr(py, intern!(py, "int"))? - .extract(py)?; - let name = x.getattr(py, intern!(py, "name"))?.extract(py)?; - atomic_parameters.insert((uuid, name)); - } - for (uuid, name) in atomic_parameters { - self.param_table - .discard_references(uuid, inst_index, *param_index, name); - } - } + instruction_index: usize, + ) -> PyResult<()> { + for (index, param) in self.data[instruction_index] + .params_view() + .iter() + .enumerate() + { + let usage = ParameterUse::Index { + instruction: instruction_index, + parameter: index as u32, + }; + for param_ob in param.iter_parameters(py)? { + self.param_table.untrack(¶m_ob?, usage)?; } } Ok(()) } + /// Retrack the entire `ParameterTable`. + /// + /// This is necessary each time an insertion or removal occurs on `self.data` other than in the + /// last position. fn reindex_parameter_table(&mut self, py: Python) -> PyResult<()> { self.param_table.clear(); for inst_index in 0..self.data.len() { - self.update_param_table(py, inst_index, None)?; + self.track_instruction_parameters(py, inst_index)?; + } + for param_ob in self.global_phase.iter_parameters(py)? { + self.param_table + .track(¶m_ob?, Some(ParameterUse::GlobalPhase))?; } - // Technically we could keep the global phase entry directly if it exists, but we're - // the incremental cost is minimal after reindexing everything. - self.global_phase(py, self.global_phase.clone())?; Ok(()) } - - pub fn append_inner(&mut self, py: Python, value: &CircuitInstruction) -> PyResult { - let packed = self.pack(py, value)?; - let new_index = self.data.len(); - self.data.push(packed); - self.update_param_table(py, new_index, None) - } } #[pymethods] @@ -346,10 +251,10 @@ impl CircuitData { cargs_interner: IndexedInterner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), - param_table: ParamTable::new(), + param_table: ParameterTable::new(), global_phase: Param::Float(0.), }; - self_.global_phase(py, global_phase)?; + self_.set_global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { self_.add_qubit(py, &bit?, true)?; @@ -430,6 +335,32 @@ impl CircuitData { self.clbits.len() } + /// Return the number of unbound compile-time symbolic parameters tracked by the circuit. + pub fn num_parameters(&self) -> usize { + self.param_table.num_parameters() + } + + /// Get a (cached) sorted list of the Python-space `Parameter` instances tracked by this circuit + /// data's parameter table. + #[getter] + pub fn get_parameters<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyList> { + self.param_table.py_parameters(py) + } + + pub fn unsorted_parameters<'py>(&self, py: Python<'py>) -> PyResult> { + self.param_table.py_parameters_unsorted(py) + } + + fn _raw_parameter_table_entry(&self, param: Bound) -> PyResult> { + self.param_table._py_raw_entry(param) + } + + pub fn get_parameter_by_name(&self, py: Python, name: PyBackedStr) -> Option> { + self.param_table + .py_parameter_by_name(&name) + .map(|ob| ob.clone_ref(py)) + } + /// Return the width of the circuit. This is the number of qubits plus the /// number of clbits. /// @@ -723,22 +654,12 @@ impl CircuitData { self.delitem(py, index.with_len(self.data.len())?) } - pub fn setitem_no_param_table_update( - &mut self, - py: Python, - index: usize, - value: &CircuitInstruction, - ) -> PyResult<()> { - self.data[index] = self.pack(py, value)?; - Ok(()) - } - pub fn __setitem__(&mut self, index: PySequenceIndex, value: &Bound) -> PyResult<()> { fn set_single(slf: &mut CircuitData, index: usize, value: &Bound) -> PyResult<()> { let py = value.py(); + slf.untrack_instruction_parameters(py, index)?; slf.data[index] = slf.pack(py, &value.downcast::()?.borrow())?; - slf.remove_from_parameter_table(py, index)?; - slf.update_param_table(py, index, None)?; + slf.track_instruction_parameters(py, index)?; Ok(()) } @@ -808,7 +729,7 @@ impl CircuitData { let packed = self.pack(py, &value)?; self.data.insert(index, packed); if index == self.data.len() - 1 { - self.update_param_table(py, index, None)?; + self.track_instruction_parameters(py, index)?; } else { self.reindex_parameter_table(py)?; } @@ -823,16 +744,40 @@ impl CircuitData { Ok(item) } - pub fn append( - &mut self, - py: Python<'_>, - value: &Bound, - params: Option)>>, - ) -> PyResult { + /// Primary entry point for appending an instruction from Python space. + pub fn append(&mut self, value: &Bound) -> PyResult<()> { + let py = value.py(); let new_index = self.data.len(); let packed = self.pack(py, &value.borrow())?; self.data.push(packed); - self.update_param_table(py, new_index, params) + self.track_instruction_parameters(py, new_index) + } + + /// Backup entry point for appending an instruction from Python space, in the unusual case that + /// one of the instruction parameters contains a cyclical reference to the circuit itself. + /// + /// In this case, the `params` field should be a list of `(index, parameters)` tuples, where the + /// index is into the instruction's `params` attribute, and `parameters` is a Python iterable + /// of `Parameter` objects. + pub fn append_manual_params( + &mut self, + value: &Bound, + params: &Bound, + ) -> PyResult<()> { + let instruction_index = self.data.len(); + let packed = self.pack(value.py(), &value.borrow())?; + self.data.push(packed); + for item in params.iter() { + let (parameter_index, parameters) = item.extract::<(u32, Bound)>()?; + let usage = ParameterUse::Index { + instruction: instruction_index, + parameter: parameter_index, + }; + for param in parameters.iter()? { + self.param_table.track(¶m?, Some(usage))?; + } + } + Ok(()) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { @@ -879,20 +824,63 @@ impl CircuitData { #[cfg(feature = "cache_pygates")] py_op: inst.py_op.clone(), }); - self.update_param_table(py, new_index, None)?; + self.track_instruction_parameters(py, new_index)?; } return Ok(()); } for v in itr.iter()? { - self.append_inner(py, &v?.downcast()?.borrow())?; + self.append(v?.downcast()?)?; } Ok(()) } - pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { + /// Assign all the circuit parameters, given a sequence-like input of `Param` instances. + fn assign_parameters_sequence(&mut self, sequence: Bound) -> PyResult<()> { + if sequence.len()? != self.param_table.num_parameters() { + return Err(PyValueError::new_err(concat!( + "Mismatching number of values and parameters. For partial binding ", + "please pass a dictionary of {parameter: value} pairs." + ))); + } + let mut old_table = std::mem::take(&mut self.param_table); + if let Ok(readonly) = sequence.extract::>() { + // Fast path for Numpy arrays; in this case we can easily handle them without copying + // the data across into a Rust-space `Vec` first. + let array = readonly.as_array(); + self.assign_parameters_inner( + sequence.py(), + array + .iter() + .zip(old_table.drain_ordered()) + .map(|(value, (param_ob, uses))| (param_ob, Param::Float(*value), uses)), + ) + } else { + let values = sequence.extract::>()?; + self.assign_parameters_inner( + sequence.py(), + values + .into_iter() + .zip(old_table.drain_ordered()) + .map(|(value, (param_ob, uses))| (param_ob, value.0, uses)), + ) + } + } + + /// Assign all uses of the circuit parameters as keys `mapping` to their corresponding values. + fn assign_parameters_mapping(&mut self, mapping: Bound) -> PyResult<()> { + let py = mapping.py(); + let mut items = Vec::new(); + for item in mapping.call_method0("items")?.iter()? { + let (param_ob, value) = item?.extract::<(Py, AssignParam)>()?; + let uuid = ParameterUuid::from_parameter(param_ob.bind(py))?; + items.push((param_ob, value.0, self.param_table.pop(uuid)?)); + } + self.assign_parameters_inner(py, items) + } + + pub fn clear(&mut self) { std::mem::take(&mut self.data); self.param_table.clear(); - Ok(()) } // Marks this pyclass as NOT hashable. @@ -939,6 +927,7 @@ impl CircuitData { // references they contain are to the bits in these lists! visit.call(self.qubits.cached())?; visit.call(self.clbits.cached())?; + self.param_table.py_gc_traverse(&visit)?; Ok(()) } @@ -947,117 +936,47 @@ impl CircuitData { self.data.clear(); self.qubits.dispose(); self.clbits.dispose(); + self.param_table.clear(); } + /// Set the global phase of the circuit. + /// + /// This method assumes that the parameter table is either fully consistent, or contains zero + /// entries for the global phase, regardless of what value is currently stored there. It's not + /// uncommon for subclasses and other parts of Qiskit to have filled in the global phase field + /// by copies or other means, before making the parameter table consistent. #[setter] - pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { - let list_builtin = BUILTIN_LIST.get_bound(py); - self.remove_from_parameter_table(py, GLOBAL_PHASE_INDEX)?; + pub fn set_global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { + if let Param::ParameterExpression(expr) = &self.global_phase { + for param_ob in expr.bind(py).getattr(intern!(py, "parameters"))?.iter()? { + match self.param_table.remove_use( + ParameterUuid::from_parameter(¶m_ob?)?, + ParameterUse::GlobalPhase, + ) { + Ok(_) + | Err(ParameterTableError::ParameterNotTracked(_)) + | Err(ParameterTableError::UsageNotTracked(_)) => (), + // Any errors added later might want propagating. + } + } + } match angle { Param::Float(angle) => { self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + Ok(()) } - Param::ParameterExpression(angle) => { - let temp: PyObject = angle.getattr(py, intern!(py, "parameters"))?; - let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; - - for (param_index, param_obj) in raw_param_objs.into_iter().enumerate() { - let param_uuid: u128 = param_obj - .getattr(py, intern!(py, "_uuid"))? - .getattr(py, intern!(py, "int"))? - .extract(py)?; - match self.param_table.table.get_mut(¶m_uuid) { - Some(entry) => entry.add(GLOBAL_PHASE_INDEX, param_index), - None => { - let new_entry = ParamEntry::new(GLOBAL_PHASE_INDEX, param_index); - self.param_table.insert(py, param_obj, new_entry)?; - } - }; + Param::ParameterExpression(_) => { + for param_ob in angle.iter_parameters(py)? { + self.param_table + .track(¶m_ob?, Some(ParameterUse::GlobalPhase))?; } - self.global_phase = Param::ParameterExpression(angle); - } - Param::Obj(_) => return Err(PyValueError::new_err("Invalid type for global phase")), - }; - Ok(()) - } - - /// Get the global_phase sentinel value - #[classattr] - pub const fn global_phase_param_index() -> usize { - GLOBAL_PHASE_INDEX - } - - // Below are functions to interact with the parameter table. These methods - // are done to avoid needing to deal with shared references and provide - // an entry point via python through an owned CircuitData object. - pub fn num_params(&self) -> usize { - self.param_table.table.len() - } - - pub fn get_param_from_name(&self, py: Python, name: String) -> Option { - self.param_table.get_param_from_name(py, name) - } - - pub fn get_params_unsorted(&self, py: Python) -> PyResult> { - Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) - } - - pub fn pop_param(&mut self, py: Python, uuid: u128, name: &str, default: PyObject) -> PyObject { - match self.param_table.pop(uuid, name) { - Some(res) => res.into_py(py), - None => default.clone_ref(py), - } - } - - pub fn _get_param(&self, py: Python, uuid: u128) -> PyObject { - self.param_table.table[&uuid].clone().into_py(py) - } - - pub fn contains_param(&self, uuid: u128) -> bool { - self.param_table.table.contains_key(&uuid) - } - - pub fn add_new_parameter( - &mut self, - py: Python, - param: PyObject, - inst_index: usize, - param_index: usize, - ) -> PyResult<()> { - self.param_table.insert( - py, - param.clone_ref(py), - ParamEntry::new(inst_index, param_index), - )?; - Ok(()) - } - - pub fn update_parameter_entry( - &mut self, - uuid: u128, - inst_index: usize, - param_index: usize, - ) -> PyResult<()> { - match self.param_table.table.get_mut(&uuid) { - Some(entry) => { - entry.add(inst_index, param_index); + self.global_phase = angle; Ok(()) } - None => Err(PyIndexError::new_err(format!( - "Invalid parameter uuid: {:?}", - uuid - ))), + Param::Obj(_) => Err(PyTypeError::new_err("invalid type for global phase")), } } - pub fn _get_entry_count(&self, py: Python, param_obj: PyObject) -> PyResult { - let uuid: u128 = param_obj - .getattr(py, intern!(py, "_uuid"))? - .getattr(py, intern!(py, "int"))? - .extract(py)?; - Ok(self.param_table.table[&uuid].index_ids.len()) - } - pub fn num_nonlocal_gates(&self) -> usize { self.data .iter() @@ -1105,4 +1024,213 @@ impl CircuitData { pub fn iter(&self) -> impl Iterator { self.data.iter() } + + fn assign_parameters_inner(&mut self, py: Python, iter: I) -> PyResult<()> + where + I: IntoIterator, Param, HashSet)>, + { + let inconsistent = + || PyRuntimeError::new_err("internal error: circuit parameter table is inconsistent"); + + let assign_attr = intern!(py, "assign"); + let assign_parameters_attr = intern!(py, "assign_parameters"); + let _definition_attr = intern!(py, "_definition"); + let numeric_attr = intern!(py, "numeric"); + let parameters_attr = intern!(py, "parameters"); + let params_attr = intern!(py, "params"); + let validate_parameter_attr = intern!(py, "validate_parameter"); + + // Bind a single `Parameter` into a Python-space `ParameterExpression`. + let bind_expr = |expr: Borrowed, + param_ob: &Py, + value: &Param, + coerce: bool| + -> PyResult { + let new_expr = expr.call_method1(assign_attr, (param_ob, value.to_object(py)))?; + if new_expr.getattr(parameters_attr)?.len()? == 0 { + let out = new_expr.call_method0(numeric_attr)?; + if coerce { + out.extract() + } else { + Param::extract_no_coerce(&out) + } + } else { + Ok(Param::ParameterExpression(new_expr.unbind())) + } + }; + + let mut user_operations = HashMap::new(); + let mut uuids = Vec::new(); + for (param_ob, value, uses) in iter { + debug_assert!(!uses.is_empty()); + uuids.clear(); + for inner_param_ob in value.iter_parameters(py)? { + uuids.push(self.param_table.track(&inner_param_ob?, None)?) + } + for usage in uses { + match usage { + ParameterUse::GlobalPhase => { + let Param::ParameterExpression(expr) = &self.global_phase else { + return Err(inconsistent()); + }; + self.set_global_phase( + py, + bind_expr(expr.bind_borrowed(py), ¶m_ob, &value, true)?, + )?; + } + ParameterUse::Index { + instruction, + parameter, + } => { + let parameter = parameter as usize; + let previous = &mut self.data[instruction]; + if let Some(standard) = previous.standard_gate() { + let params = previous.params_mut(); + let Param::ParameterExpression(expr) = ¶ms[parameter] else { + return Err(inconsistent()); + }; + params[parameter] = + match bind_expr(expr.bind_borrowed(py), ¶m_ob, &value, true)? { + Param::Obj(obj) => { + return Err(CircuitError::new_err(format!( + "bad type after binding for gate '{}': '{}'", + standard.name(), + obj.bind(py).repr()?, + ))) + } + param => param, + }; + for uuid in uuids.iter() { + self.param_table.add_use(*uuid, usage)? + } + #[cfg(feature = "cache_pygates")] + { + // Standard gates can all rebuild their definitions, so if the + // cached py_op exists, just clear out any existing cache. + if let Some(borrowed) = previous.py_op.borrow().as_ref() { + borrowed.bind(py).setattr("_definition", py.None())? + } + } + } else { + // Track user operations we've seen so we can rebind their definitions. + // Strictly this can add the same binding pair more than once, if an + // instruction has the same `Parameter` in several of its `params`, but + // we're going to turn that into a `dict` anyway, so it doesn't matter. + user_operations + .entry(instruction) + .or_insert_with(Vec::new) + .push((param_ob.clone_ref(py), value.clone())); + + let op = previous.unpack_py_op(py)?.into_bound(py); + let previous_param = &previous.params_view()[parameter]; + let new_param = match previous_param { + Param::Float(_) => return Err(inconsistent()), + Param::ParameterExpression(expr) => { + // For user gates, we don't coerce floats to integers in `Param` + // so that users can use them if they choose. + let new_param = bind_expr( + expr.bind_borrowed(py), + ¶m_ob, + &value, + false, + )?; + // Historically, `assign_parameters` called `validate_parameter` + // only when a `ParameterExpression` became fully bound. Some + // "generalised" (or user) gates fail without this, though + // arguably, that's them indicating they shouldn't be allowed to + // be parametric. + // + // Our `bind_expr` coercion means that a non-parametric + // `ParameterExperssion` after binding would have been coerced + // to a numeric quantity already, so the match here is + // definitely parameterized. + match new_param { + Param::ParameterExpression(_) => new_param, + new_param => Param::extract_no_coerce(&op.call_method1( + validate_parameter_attr, + (new_param,), + )?)?, + } + } + Param::Obj(obj) => { + let obj = obj.bind_borrowed(py); + if !obj.is_instance(QUANTUM_CIRCUIT.get_bound(py))? { + return Err(inconsistent()); + } + Param::extract_no_coerce( + &obj.call_method( + assign_parameters_attr, + ([(¶m_ob, &value)].into_py_dict_bound(py),), + Some( + &[("inplace", false), ("flat_input", true)] + .into_py_dict_bound(py), + ), + )?, + )? + } + }; + op.getattr(params_attr)?.set_item(parameter, new_param)?; + let mut new_op = op.extract::()?; + previous.op = new_op.operation; + previous.params_mut().swap_with_slice(&mut new_op.params); + previous.extra_attrs = new_op.extra_attrs; + #[cfg(feature = "cache_pygates")] + { + *previous.py_op.borrow_mut() = Some(op.into_py(py)); + } + for uuid in uuids.iter() { + self.param_table.add_use(*uuid, usage)? + } + } + } + } + } + } + + let assign_kwargs = (!user_operations.is_empty()).then(|| { + [("inplace", true), ("flat_input", true), ("strict", false)].into_py_dict_bound(py) + }); + for (instruction, bindings) in user_operations { + // We only put non-standard gates in `user_operations`, so we're not risking creating a + // previously non-existent Python object. + let instruction = &self.data[instruction]; + let definition_cache = if matches!(instruction.op.view(), OperationRef::Operation(_)) { + // `Operation` instances don't have a `definition` as part of their interfaces, but + // they might be an `AnnotatedOperation`, which is one of our special built-ins. + // This should be handled more completely in the user-customisation interface by a + // delegating method, but that's not the data model we currently have. + let py_op = instruction.unpack_py_op(py)?; + let py_op = py_op.bind(py); + if !py_op.is_instance(ANNOTATED_OPERATION.get_bound(py))? { + continue; + } + py_op + .getattr(intern!(py, "base_op"))? + .getattr(_definition_attr)? + } else { + instruction + .unpack_py_op(py)? + .bind(py) + .getattr(_definition_attr)? + }; + if !definition_cache.is_none() { + definition_cache.call_method( + assign_parameters_attr, + (bindings.into_py_dict_bound(py),), + assign_kwargs.as_ref(), + )?; + } + } + Ok(()) + } +} + +/// Helper struct for `assign_parameters` to allow use of `Param::extract_no_coerce` in +/// PyO3-provided `FromPyObject` implementations on containers. +#[repr(transparent)] +struct AssignParam(Param); +impl<'py> FromPyObject<'py> for AssignParam { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + Ok(Self(Param::extract_no_coerce(ob)?)) + } } diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 3a1c6ff5ff4a..d9d439bb4745 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -75,9 +75,12 @@ pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); pub static CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ControlledGate"); +pub static ANNOTATED_OPERATION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit", "AnnotatedOperation"); pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static QI_OPERATOR: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Operator"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); +pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index caf905a8c036..7134662e1ac6 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -25,7 +25,7 @@ use smallvec::smallvec; use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyTuple}; +use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyTuple}; use pyo3::{intern, IntoPy, Python}; #[derive(Clone, Debug)] @@ -37,17 +37,13 @@ pub enum Param { impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { - Ok( - if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? - || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? - { - Param::ParameterExpression(b.clone().unbind()) - } else if let Ok(val) = b.extract::() { - Param::Float(val) - } else { - Param::Obj(b.clone().unbind()) - }, - ) + Ok(if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? { + Param::ParameterExpression(b.clone().unbind()) + } else if let Ok(val) = b.extract::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }) } } @@ -71,6 +67,52 @@ impl ToPyObject for Param { } } +impl Param { + /// Get an iterator over any Python-space `Parameter` instances tracked within this `Param`. + pub fn iter_parameters<'py>(&self, py: Python<'py>) -> PyResult> { + let parameters_attr = intern!(py, "parameters"); + match self { + Param::Float(_) => Ok(ParamParameterIter(None)), + Param::ParameterExpression(expr) => Ok(ParamParameterIter(Some( + expr.bind(py).getattr(parameters_attr)?.iter()?, + ))), + Param::Obj(obj) => { + let obj = obj.bind(py); + if obj.is_instance(QUANTUM_CIRCUIT.get_bound(py))? { + Ok(ParamParameterIter(Some( + obj.getattr(parameters_attr)?.iter()?, + ))) + } else { + Ok(ParamParameterIter(None)) + } + } + } + } + + /// Extract from a Python object without numeric coercion to float. The default conversion will + /// coerce integers into floats, but in things like `assign_parameters`, this is not always + /// desirable. + pub fn extract_no_coerce(ob: &Bound) -> PyResult { + Ok(if ob.is_instance_of::() { + Param::Float(ob.extract()?) + } else if ob.is_instance(PARAMETER_EXPRESSION.get_bound(ob.py()))? { + Param::ParameterExpression(ob.clone().unbind()) + } else { + Param::Obj(ob.clone().unbind()) + }) + } +} + +/// Struct to provide iteration over Python-space `Parameter` instances within a `Param`. +pub struct ParamParameterIter<'py>(Option>); +impl<'py> Iterator for ParamParameterIter<'py> { + type Item = PyResult>; + + fn next(&mut self) -> Option { + self.0.as_mut().and_then(|iter| iter.next()) + } +} + /// Trait for generic circuit operations these define the common attributes /// needed for something to be addable to the circuit struct pub trait Operation { diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 9e5b31245391..8825fbd71772 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -10,164 +10,395 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use hashbrown::hash_map::Entry; +use hashbrown::{HashMap, HashSet}; +use thiserror::Error; + +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; -use pyo3::{import_exception, intern, PyObject}; +use pyo3::pybacked::PyBackedStr; +use pyo3::types::{PyList, PySet}; +use pyo3::{import_exception, intern, PyTraverseError, PyVisit}; -import_exception!(qiskit.circuit.exceptions, CircuitError); +use crate::imports::UUID; -use hashbrown::{HashMap, HashSet}; +import_exception!(qiskit.circuit, CircuitError); + +#[derive(Error, Debug)] +pub enum ParameterTableError { + #[error("parameter '{0:?}' is not tracked in the table")] + ParameterNotTracked(ParameterUuid), + #[error("usage {0:?} is not tracked by the table")] + UsageNotTracked(ParameterUse), +} +impl From for PyErr { + fn from(value: ParameterTableError) -> PyErr { + CircuitError::new_err(value.to_string()) + } +} + +/// A single use of a symbolic parameter. +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub enum ParameterUse { + Index { instruction: usize, parameter: u32 }, + GlobalPhase, +} + +/// Rust-space extra information that a `ParameterVectorElement` has. This is used most heavily +/// during sorting; vector elements are sorted by their parent name, and index within that. +#[derive(Clone, Debug)] +struct VectorElement { + vector_uuid: VectorUuid, + index: usize, +} + +/// Tracked data tied to each parameter's UUID in the table. +#[derive(Clone, Debug)] +pub struct ParameterInfo { + uses: HashSet, + name: PyBackedStr, + element: Option, + object: Py, +} -/// The index value in a `ParamEntry` that indicates the global phase. -pub const GLOBAL_PHASE_INDEX: usize = usize::MAX; +/// Rust-space information on a Python `ParameterVector` and its uses in the table. +#[derive(Clone, Debug)] +struct VectorInfo { + name: PyBackedStr, + /// Number of elements of the vector tracked within the parameter table. + refcount: usize, +} -#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] -pub(crate) struct ParamEntryKeys { - keys: Vec<(usize, usize)>, - iter_pos: usize, +/// Type-safe UUID for a symbolic parameter. This does not track the name of the `Parameter`; it +/// can't be used alone to reconstruct a Python instance. That tracking remains only withing the +/// `ParameterTable`. +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub struct ParameterUuid(u128); +impl ParameterUuid { + /// Extract a UUID from a Python-space `Parameter` object. This assumes that the object is known + /// to be a parameter. + pub fn from_parameter(ob: &Bound) -> PyResult { + ob.getattr(intern!(ob.py(), "_uuid"))?.extract() + } } -#[pymethods] -impl ParamEntryKeys { - fn __iter__(slf: PyRef) -> Py { - slf.into() +/// This implementation of `FromPyObject` is for the UUID itself, which is what the `ParameterUuid` +/// struct actually encapsulates. +impl<'py> FromPyObject<'py> for ParameterUuid { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + if ob.is_exact_instance(UUID.get_bound(ob.py())) { + ob.getattr(intern!(ob.py(), "int"))?.extract().map(Self) + } else { + Err(PyTypeError::new_err("not a UUID")) + } } +} - fn __next__(mut slf: PyRefMut) -> Option<(usize, usize)> { - if slf.iter_pos < slf.keys.len() { - let res = Some(slf.keys[slf.iter_pos]); - slf.iter_pos += 1; - res +/// Type-safe UUID for a parameter vector. This is just used internally for tracking. +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +struct VectorUuid(u128); +impl VectorUuid { + /// Extract a UUID from a Python-space `ParameterVector` object. This assumes that the object is + /// the correct type. + fn from_vector(ob: &Bound) -> PyResult { + ob.getattr(intern!(ob.py(), "_root_uuid"))?.extract() + } +} +impl<'py> FromPyObject<'py> for VectorUuid { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + if ob.is_exact_instance(UUID.get_bound(ob.py())) { + ob.getattr(intern!(ob.py(), "int"))?.extract().map(Self) } else { - None + Err(PyTypeError::new_err("not a UUID")) } } } -#[derive(Clone, Debug)] -#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] -pub(crate) struct ParamEntry { - /// Mapping of tuple of instruction index (in CircuitData) and parameter index to the actual - /// parameter object - pub index_ids: HashSet<(usize, usize)>, +#[derive(Clone, Default, Debug)] +pub struct ParameterTable { + /// Mapping of the parameter key (its UUID) to the information on it tracked by this table. + by_uuid: HashMap, + /// Mapping of the parameter names to the UUID that represents them. Since we always get + /// parameters in as Python-space objects, we use the string object extracted from Python space. + by_name: HashMap, + /// Additional information on any `ParameterVector` instances that have elements in the circuit. + vectors: HashMap, + /// Sort order of the parameters. This is lexicographical for most parameters, except elements + /// of a `ParameterVector` are sorted within the vector by numerical index. We calculate this + /// on demand and cache it; an empty `order` implies it is not currently calculated. We don't + /// use `Option` so we can re-use the allocation for partial parameter bindings. + /// + /// Any method that adds or a removes a parameter is responsible for invalidating this cache. + order: Vec, + /// Cache of a Python-space list of the parameter objects, in order. We only generate this + /// specifically when asked. + /// + /// Any method that adds or a removes a parameter is responsible for invalidating this cache. + py_parameters: Option>, } -impl ParamEntry { - pub fn add(&mut self, inst_index: usize, param_index: usize) { - self.index_ids.insert((inst_index, param_index)); +impl ParameterTable { + pub fn new() -> Self { + Default::default() } - pub fn discard(&mut self, inst_index: usize, param_index: usize) { - self.index_ids.remove(&(inst_index, param_index)); + /// Get the number of parameters tracked by the table. + pub fn num_parameters(&self) -> usize { + self.by_uuid.len() } -} -#[pymethods] -impl ParamEntry { - #[new] - pub fn new(inst_index: usize, param_index: usize) -> Self { - ParamEntry { - index_ids: HashSet::from([(inst_index, param_index)]), + /// Add a new usage of a parameter coming in from Python space, optionally adding a first usage + /// to it. + /// + /// The no-use form is useful when doing parameter assignments from Rust space, where the + /// replacement is itself parametric; the replacement can be extracted once, then subsequent + /// lookups and updates done without interaction with Python. + pub fn track( + &mut self, + param_ob: &Bound, + usage: Option, + ) -> PyResult { + let py = param_ob.py(); + let uuid = ParameterUuid::from_parameter(param_ob)?; + match self.by_uuid.entry(uuid) { + Entry::Occupied(mut entry) => { + if let Some(usage) = usage { + entry.get_mut().uses.insert(usage); + } + } + Entry::Vacant(entry) => { + let py_name_attr = intern!(py, "name"); + let name = param_ob.getattr(py_name_attr)?.extract::()?; + if self.by_name.contains_key(&name) { + return Err(CircuitError::new_err(format!( + "name conflict adding parameter '{}'", + &name + ))); + } + let element = if let Ok(vector) = param_ob.getattr(intern!(py, "vector")) { + let vector_uuid = VectorUuid::from_vector(&vector)?; + match self.vectors.entry(vector_uuid) { + Entry::Occupied(mut entry) => entry.get_mut().refcount += 1, + Entry::Vacant(entry) => { + entry.insert(VectorInfo { + name: vector.getattr(py_name_attr)?.extract()?, + refcount: 1, + }); + } + } + Some(VectorElement { + vector_uuid, + index: param_ob.getattr(intern!(py, "index"))?.extract()?, + }) + } else { + None + }; + self.by_name.insert(name.clone(), uuid); + self.order.clear(); + self.py_parameters = None; + let mut uses = HashSet::new(); + if let Some(usage) = usage { + uses.insert_unique_unchecked(usage); + }; + entry.insert(ParameterInfo { + name, + uses, + element, + object: param_ob.clone().unbind(), + }); + } } + Ok(uuid) } - pub fn __len__(&self) -> usize { - self.index_ids.len() + /// Untrack one use of a single Python-space `Parameter` object from the table, discarding all + /// other tracking of that `Parameter` if this was the last usage of it. + pub fn untrack(&mut self, param_ob: &Bound, usage: ParameterUse) -> PyResult<()> { + self.remove_use(ParameterUuid::from_parameter(param_ob)?, usage) + .map_err(PyErr::from) } - pub fn __contains__(&self, key: (usize, usize)) -> bool { - self.index_ids.contains(&key) + /// Lookup the Python parameter object by name. + pub fn py_parameter_by_name(&self, name: &PyBackedStr) -> Option<&Py> { + self.by_name + .get(name) + .map(|uuid| &self.by_uuid[uuid].object) } - pub fn __iter__(&self) -> ParamEntryKeys { - ParamEntryKeys { - keys: self.index_ids.iter().copied().collect(), - iter_pos: 0, + /// Get the (maybe cached) Python list of the sorted `Parameter` objects. + pub fn py_parameters<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyList> { + if let Some(py_parameters) = self.py_parameters.as_ref() { + return py_parameters.clone_ref(py).into_bound(py); } + self.ensure_sorted(); + let out = PyList::new_bound( + py, + self.order + .iter() + .map(|uuid| self.by_uuid[uuid].object.clone_ref(py).into_bound(py)), + ); + self.py_parameters = Some(out.clone().unbind()); + out } -} -#[derive(Clone, Debug)] -#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] -pub(crate) struct ParamTable { - /// Mapping of parameter uuid (as an int) to the Parameter Entry - pub table: HashMap, - /// Mapping of parameter name to uuid as an int - pub names: HashMap, - /// Mapping of uuid to a parameter object - pub uuid_map: HashMap, -} + /// Get a Python set of all tracked `Parameter` objects. + pub fn py_parameters_unsorted<'py>(&self, py: Python<'py>) -> PyResult> { + PySet::new_bound(py, self.by_uuid.values().map(|info| &info.object)) + } -impl ParamTable { - pub fn insert(&mut self, py: Python, parameter: PyObject, entry: ParamEntry) -> PyResult<()> { - let uuid: u128 = parameter - .getattr(py, intern!(py, "_uuid"))? - .getattr(py, intern!(py, "int"))? - .extract(py)?; - let name: String = parameter.getattr(py, intern!(py, "name"))?.extract(py)?; - - if self.names.contains_key(&name) && !self.table.contains_key(&uuid) { - return Err(CircuitError::new_err(format!( - "Name conflict on adding parameter: {}", - name - ))); + /// Ensure that the `order` field is populated and sorted. + fn ensure_sorted(&mut self) { + // If `order` is already populated, it's sorted; it's the responsibility of the methods of + // this struct that mutate it to invalidate the cache. + if !self.order.is_empty() { + return; } - self.table.insert(uuid, entry); - self.names.insert(name, uuid); - self.uuid_map.insert(uuid, parameter); + self.order.reserve(self.by_uuid.len()); + self.order.extend(self.by_uuid.keys()); + self.order.sort_unstable_by_key(|uuid| { + let info = &self.by_uuid[uuid]; + if let Some(vec) = info.element.as_ref() { + (&self.vectors[&vec.vector_uuid].name, vec.index) + } else { + (&info.name, 0) + } + }) + } + + /// Add a use of a parameter to the table. + pub fn add_use( + &mut self, + uuid: ParameterUuid, + usage: ParameterUse, + ) -> Result<(), ParameterTableError> { + self.by_uuid + .get_mut(&uuid) + .ok_or(ParameterTableError::ParameterNotTracked(uuid))? + .uses + .insert(usage); Ok(()) } - pub fn discard_references( + /// Return a use of a parameter. + /// + /// If the last use a parameter is discarded, the parameter is untracked. + pub fn remove_use( &mut self, - uuid: u128, - inst_index: usize, - param_index: usize, - name: String, - ) { - if let Some(refs) = self.table.get_mut(&uuid) { - if refs.__len__() == 1 { - self.table.remove(&uuid); - self.names.remove(&name); - self.uuid_map.remove(&uuid); - } else { - refs.discard(inst_index, param_index); + uuid: ParameterUuid, + usage: ParameterUse, + ) -> Result<(), ParameterTableError> { + let Entry::Occupied(mut entry) = self.by_uuid.entry(uuid) else { + return Err(ParameterTableError::ParameterNotTracked(uuid)); + }; + let info = entry.get_mut(); + if !info.uses.remove(&usage) { + return Err(ParameterTableError::UsageNotTracked(usage)); + } + if info.uses.is_empty() { + self.by_name.remove(&info.name); + if let Some(vec) = info.element.as_ref() { + let Entry::Occupied(mut vec_entry) = self.vectors.entry(vec.vector_uuid) else { + unreachable!() + }; + vec_entry.get_mut().refcount -= 1; + if vec_entry.get().refcount == 0 { + vec_entry.remove_entry(); + } } + self.order.clear(); + self.py_parameters = None; + entry.remove_entry(); } + Ok(()) } -} -#[pymethods] -impl ParamTable { - #[new] - pub fn new() -> Self { - ParamTable { - table: HashMap::new(), - names: HashMap::new(), - uuid_map: HashMap::new(), + /// Remove a parameter from the table, returning the tracked uses of it. + pub fn pop( + &mut self, + uuid: ParameterUuid, + ) -> Result, ParameterTableError> { + let info = self + .by_uuid + .remove(&uuid) + .ok_or(ParameterTableError::ParameterNotTracked(uuid))?; + self.by_name + .remove(&info.name) + .expect("each parameter should be tracked by both UUID and name"); + if let Some(element) = info.element { + self.vectors + .entry(element.vector_uuid) + .and_replace_entry_with(|_k, mut vector_info| { + vector_info.refcount -= 1; + (vector_info.refcount > 0).then_some(vector_info) + }); } + self.order.clear(); + self.py_parameters = None; + Ok(info.uses) } - pub fn clear(&mut self) { - self.table.clear(); - self.names.clear(); - self.uuid_map.clear(); + /// Clear this table, yielding the Python parameter objects and their uses in sorted order. + pub fn drain_ordered( + &'_ mut self, + ) -> impl Iterator, HashSet)> + '_ { + self.ensure_sorted(); + self.by_name.clear(); + self.vectors.clear(); + self.py_parameters = None; + self.order.drain(..).map(|uuid| { + let info = self + .by_uuid + .remove(&uuid) + .expect("tracked UUIDs should be consistent"); + (info.object, info.uses) + }) } - pub fn pop(&mut self, key: u128, name: &str) -> Option { - self.names.remove(name); - self.uuid_map.remove(&key); - self.table.remove(&key) + /// Empty this `ParameterTable` of all its contents. This does not affect the capacities of the + /// internal data storage. + pub fn clear(&mut self) { + self.by_uuid.clear(); + self.by_name.clear(); + self.vectors.clear(); + self.order.clear(); + self.py_parameters = None; } - fn set(&mut self, uuid: u128, name: String, param: PyObject, refs: ParamEntry) { - self.names.insert(name, uuid); - self.table.insert(uuid, refs); - self.uuid_map.insert(uuid, param); + /// Expose the tracked data for a given parameter as directly as possible to Python space. + /// + /// This is only really intended for use in testing. + pub(crate) fn _py_raw_entry(&self, param: Bound) -> PyResult> { + let py = param.py(); + let uuid = ParameterUuid::from_parameter(¶m)?; + let info = self + .by_uuid + .get(&uuid) + .ok_or(ParameterTableError::ParameterNotTracked(uuid))?; + // PyO3's `PySet::new_bound` only accepts iterables of references. + let out = PySet::empty_bound(py)?; + for usage in info.uses.iter() { + match usage { + ParameterUse::GlobalPhase => out.add((py.None(), py.None()))?, + ParameterUse::Index { + instruction, + parameter, + } => out.add((*instruction, *parameter))?, + } + } + Ok(out.unbind()) } - pub fn get_param_from_name(&self, py: Python, name: String) -> Option { - self.names - .get(&name) - .map(|x| self.uuid_map.get(x).map(|y| y.clone_ref(py)))? + /// Accept traversal of this object by the Python garbage collector. + /// + /// This is not a pyclass, so it's up to our owner to delegate their own traversal to us. + pub fn py_gc_traverse(&self, visit: &PyVisit) -> Result<(), PyTraverseError> { + for info in self.by_uuid.values() { + visit.call(&info.object)? + } + // We don't need to / can't visit the `PyBackedStr` stores. + if let Some(list) = self.py_parameters.as_ref() { + visit.call(list)? + } + Ok(()) } } diff --git a/qiskit/circuit/_utils.py b/qiskit/circuit/_utils.py index 86a058e88525..db49cbf90574 100644 --- a/qiskit/circuit/_utils.py +++ b/qiskit/circuit/_utils.py @@ -19,19 +19,6 @@ from qiskit import _numpy_compat from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError -from .parametervector import ParameterVectorElement - - -def sort_parameters(parameters): - """Sort an iterable of :class:`.Parameter` instances into a canonical order, respecting the - ordering relationships between elements of :class:`.ParameterVector`\\ s.""" - - def key(parameter): - if isinstance(parameter, ParameterVectorElement): - return (parameter.vector.name, parameter.index) - return (parameter.name,) - - return sorted(parameters, key=key) def _compute_control_matrix(base_mat, num_ctrl_qubits, ctrl_state=None): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 3b2da762c51f..ad6353373358 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -15,6 +15,8 @@ """Quantum circuit object.""" from __future__ import annotations + +import collections.abc import copy as _copy import itertools import multiprocessing as mp @@ -46,7 +48,6 @@ from qiskit.circuit.exceptions import CircuitError from qiskit.utils import deprecate_func from . import _classical_resource_map -from ._utils import sort_parameters from .controlflow import ControlFlowOp, _builder_utils from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder @@ -1127,9 +1128,6 @@ def __init__( self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict) self.add_register(*regs) - # Cache to avoid re-sorting parameters - self._parameters = None - self._layout = None self.global_phase = global_phase @@ -1266,7 +1264,6 @@ def data(self, data_input: Iterable): else: data_input = list(data_input) self._data.clear() - self._parameters = None # Repopulate the parameter table with any global-phase entries. self.global_phase = self.global_phase if not data_input: @@ -2568,9 +2565,7 @@ def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = Fal :meta public: """ if _standard_gate: - new_param = self._data.append(instruction) - if new_param: - self._parameters = None + self._data.append(instruction) self.duration = None self.unit = "dt" return instruction @@ -2582,19 +2577,16 @@ def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = Fal # instruction param the inner rust append method will raise a runtime error. # When this happens we need to handle the parameters separately. # This shouldn't happen in practice but 2 tests were doing this and it's not - # explicitly prohibted by the API so this and the `params` optional argument - # path guard against it. + # explicitly prohibted by the API. try: - new_param = self._data.append(instruction) + self._data.append(instruction) except RuntimeError: - params = [] - for idx, param in enumerate(instruction.operation.params): - if isinstance(param, (ParameterExpression, QuantumCircuit)): - params.append((idx, list(set(param.parameters)))) - new_param = self._data.append(instruction, params) - if new_param: - # clear cache if new parameter is added - self._parameters = None + params = [ + (idx, param.parameters) + for idx, param in enumerate(instruction.operation.params) + if isinstance(param, (ParameterExpression, QuantumCircuit)) + ] + self._data.append_manual_params(instruction, params) # Invalidate whole circuit duration if an instruction is added self.duration = None @@ -2650,7 +2642,7 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: A similar method, but for :class:`.expr.Var` run-time variables instead of :class:`.Parameter` compile-time parameters. """ - if (parameter := self._data.get_param_from_name(name)) is None: + if (parameter := self._data.get_parameter_by_name(name)) is None: if default is Ellipsis: raise KeyError(f"no parameter named '{name}' is present") return default @@ -3722,8 +3714,6 @@ def copy_empty_like( cpy._data = CircuitData( self._data.qubits, self._data.clbits, global_phase=self._data.global_phase ) - # Invalidate parameters caching. - cpy._parameters = None cpy._calibrations = _copy.deepcopy(self._calibrations) cpy._metadata = _copy.deepcopy(self._metadata) @@ -4110,16 +4100,6 @@ def global_phase(self, angle: ParameterValueType): Args: angle (float, ParameterExpression): radians """ - # If we're currently parametric, we need to throw away the references. This setter is - # called by some subclasses before the inner `_global_phase` is initialized. - if isinstance(getattr(self._data, "global_phase", None), ParameterExpression): - self._parameters = None - if isinstance(angle, ParameterExpression): - if angle.parameters: - self._parameters = None - else: - angle = _normalize_global_phase(angle) - if self._control_flow_scopes: self._control_flow_scopes[-1].global_phase = angle else: @@ -4184,16 +4164,13 @@ def parameters(self) -> ParameterView: Returns: The sorted :class:`.Parameter` objects in the circuit. """ - # parameters from gates - if self._parameters is None: - self._parameters = sort_parameters(self._unsorted_parameters()) # return as parameter view, which implements the set and list interface - return ParameterView(self._parameters) + return ParameterView(self._data.parameters) @property def num_parameters(self) -> int: """The number of parameter objects in the circuit.""" - return self._data.num_params() + return self._data.num_parameters() def _unsorted_parameters(self) -> set[Parameter]: """Efficiently get all parameters in the circuit, without any sorting overhead. @@ -4206,7 +4183,7 @@ def _unsorted_parameters(self) -> set[Parameter]: """ # This should be free, by accessing the actual backing data structure of the table, but that # means that we need to copy it if adding keys from the global phase. - return self._data.get_params_unsorted() + return self._data.unsorted_parameters() @overload def assign_parameters( @@ -4319,108 +4296,28 @@ def assign_parameters( # pylint: disable=missing-raises-doc if inplace: target = self else: + if not isinstance(parameters, dict): + # We're going to need to access the sorted order wihin the inner Rust method on + # `target`, so warm up our own cache first so that subsequent calls to + # `assign_parameters` on `self` benefit as well. + _ = self._data.parameters target = self.copy() target._increment_instances() target._name_update() - # Normalize the inputs into simple abstract interfaces, so we've dispatched the "iteration" - # logic in one place at the start of the function. This lets us do things like calculate - # and cache expensive properties for (e.g.) the sequence format only if they're used; for - # many large, close-to-hardware circuits, we won't need the extra handling for - # `global_phase` or recursive definition binding. - # - # During normalisation, be sure to reference 'parameters' and related things from 'self' not - # 'target' so we can take advantage of any caching we might be doing. - if isinstance(parameters, dict): + if isinstance(parameters, collections.abc.Mapping): raw_mapping = parameters if flat_input else self._unroll_param_dict(parameters) - # Remember that we _must not_ mutate the output of `_unsorted_parameters`. - our_parameters = self._unsorted_parameters() + our_parameters = self._data.unsorted_parameters() if strict and (extras := raw_mapping.keys() - our_parameters): raise CircuitError( f"Cannot bind parameters ({', '.join(str(x) for x in extras)}) not present in" " the circuit." ) parameter_binds = _ParameterBindsDict(raw_mapping, our_parameters) + target._data.assign_parameters_mapping(parameter_binds) else: - our_parameters = self.parameters - if len(parameters) != len(our_parameters): - raise ValueError( - "Mismatching number of values and parameters. For partial binding " - "please pass a dictionary of {parameter: value} pairs." - ) - parameter_binds = _ParameterBindsSequence(our_parameters, parameters) - - # Clear out the parameter table for the relevant entries, since we'll be binding those. - # Any new references to parameters are reinserted as part of the bind. - target._parameters = None - # This is deliberately eager, because we want the side effect of clearing the table. - all_references = [ - (parameter, value, target._data.pop_param(parameter.uuid.int, parameter.name, ())) - for parameter, value in parameter_binds.items() - ] - seen_operations = {} - # The meat of the actual binding for regular operations. - for to_bind, bound_value, references in all_references: - update_parameters = ( - tuple(bound_value.parameters) - if isinstance(bound_value, ParameterExpression) - else () - ) - for inst_index, index in references: - if inst_index == self._data.global_phase_param_index: - operation = None - seen_operations[inst_index] = None - assignee = target.global_phase - validate = _normalize_global_phase - else: - operation = target._data[inst_index].operation - seen_operations[inst_index] = operation - assignee = operation.params[index] - validate = operation.validate_parameter - if isinstance(assignee, ParameterExpression): - new_parameter = assignee.assign(to_bind, bound_value) - for parameter in update_parameters: - if not target._data.contains_param(parameter.uuid.int): - target._data.add_new_parameter(parameter, inst_index, index) - else: - target._data.update_parameter_entry( - parameter.uuid.int, - inst_index, - index, - ) - if not new_parameter.parameters: - new_parameter = validate(new_parameter.numeric()) - elif isinstance(assignee, QuantumCircuit): - new_parameter = assignee.assign_parameters( - {to_bind: bound_value}, inplace=False, flat_input=True - ) - else: - raise RuntimeError( # pragma: no cover - f"Saw an unknown type during symbolic binding: {assignee}." - " This may indicate an internal logic error in symbol tracking." - ) - if inst_index == self._data.global_phase_param_index: - # We've already handled parameter table updates in bulk, so we need to skip the - # public setter trying to do it again. - target._data.global_phase = new_parameter - else: - temp_params = operation.params - temp_params[index] = new_parameter - operation.params = temp_params - target._data.setitem_no_param_table_update( - inst_index, - target._data[inst_index].replace(operation=operation, params=temp_params), - ) - - # After we've been through everything at the top level, make a single visit to each - # operation we've seen, rebinding its definition if necessary. - for operation in seen_operations.values(): - if ( - definition := getattr(operation, "_definition", None) - ) is not None and definition.num_parameters: - definition.assign_parameters( - parameter_binds.mapping, inplace=True, flat_input=True, strict=False - ) + parameter_binds = _ParameterBindsSequence(target._data.parameters, parameters) + target._data.assign_parameters_sequence(parameters) # Finally, assign the parameters inside any of the calibrations. We don't track these in # the `ParameterTable`, so we manually reconstruct things. @@ -4457,7 +4354,6 @@ def map_calibration(qubits, parameters, schedule): for gate, calibrations in target._calibrations.items() ), ) - target._parameters = None return None if inplace else target def _unroll_param_dict( @@ -6054,7 +5950,6 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: if not self._data: raise CircuitError("This circuit contains no instructions.") instruction = self._data.pop() - self._parameters = None return instruction @typing.overload @@ -6708,7 +6603,6 @@ def append(self, instruction, *, _standard_gate: bool = False): def extend(self, data: CircuitData): self.circuit._data.extend(data) - self.circuit._parameters = None self.circuit.duration = None self.circuit.unit = "dt" @@ -6867,11 +6761,3 @@ def _bit_argument_conversion_scalar(specifier, bit_sequence, bit_set, type_): else f"Invalid bit index: '{specifier}' of type '{type(specifier)}'" ) raise CircuitError(message) - - -def _normalize_global_phase(angle): - """Return the normalized form of an angle for use in the global phase. This coerces to float if - possible, and fixes to the interval :math:`[0, 2\\pi)`.""" - if isinstance(angle, ParameterExpression) and angle.parameters: - return angle - return float(angle) % (2.0 * np.pi) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index b7a4aaea65a2..098ab8578d48 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1196,7 +1196,9 @@ def is_loop_variable(circuit, parameter): # _should_ be an intrinsic part of the parameter, or somewhere publicly accessible, but # Terra doesn't have those concepts yet. We can only try and guess at the type by looking # at all the places it's used in the circuit. - for instr_index, index in circuit._data._get_param(parameter.uuid.int): + for instr_index, index in circuit._data._raw_parameter_table_entry(parameter): + if instr_index is None: + continue instruction = circuit.data[instr_index].operation if isinstance(instruction, ForLoopOp): # The parameters of ForLoopOp are (indexset, loop_parameter, body). diff --git a/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 5f0a2814872f..e7f67dae2c82 100644 --- a/test/python/circuit/library/test_blueprintcircuit.py +++ b/test/python/circuit/library/test_blueprintcircuit.py @@ -77,17 +77,17 @@ def test_invalidate_rebuild(self): with self.subTest(msg="after building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(mock._data.num_params(), 1) + self.assertEqual(mock._data.num_parameters(), 1) mock._invalidate() with self.subTest(msg="after invalidating"): self.assertFalse(mock._is_built) - self.assertEqual(mock._data.num_params(), 0) + self.assertEqual(mock._data.num_parameters(), 0) mock._build() with self.subTest(msg="after re-building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(mock._data.num_params(), 1) + self.assertEqual(mock._data.num_parameters(), 1) def test_calling_attributes_works(self): """Test that the circuit is constructed when attributes are called.""" diff --git a/test/python/circuit/library/test_pauli_feature_map.py b/test/python/circuit/library/test_pauli_feature_map.py index c43c2b5f6f1f..60b7e5c28b9c 100644 --- a/test/python/circuit/library/test_pauli_feature_map.py +++ b/test/python/circuit/library/test_pauli_feature_map.py @@ -213,9 +213,9 @@ def test_parameter_prefix(self): "ParameterView([ParameterVectorElement(x[0]), ParameterVectorElement(x[1])])", ) - encoding_pauli_param_y = encoding_pauli.assign_parameters({1, y}) - encoding_z_param_y = encoding_z.assign_parameters({1, y}) - encoding_zz_param_y = encoding_zz.assign_parameters({1, y}) + encoding_pauli_param_y = encoding_pauli.assign_parameters([1, y]) + encoding_z_param_y = encoding_z.assign_parameters([1, y]) + encoding_zz_param_y = encoding_zz.assign_parameters([1, y]) self.assertEqual(str(encoding_pauli_param_y.parameters), "ParameterView([Parameter(y)])") self.assertEqual(str(encoding_z_param_y.parameters), "ParameterView([Parameter(y)])") diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index e75d67ed5dc1..ff241baeb61c 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -859,6 +859,6 @@ def test_param_gate_instance(self): # A fancy way of doing qc0_instance = qc0.data[0] and qc1_instance = qc1.data[0] # but this at least verifies the parameter table is point from the parameter to # the correct instruction (which is the only one) - qc0_instance = qc0._data[next(iter(qc0._data._get_param(b.uuid.int)))[0]] - qc1_instance = qc1._data[next(iter(qc1._data._get_param(a.uuid.int)))[0]] + qc0_instance = qc0._data[next(iter(qc0._data._raw_parameter_table_entry(b)))[0]] + qc1_instance = qc1._data[next(iter(qc1._data._raw_parameter_table_entry(a)))[0]] self.assertNotEqual(qc0_instance, qc1_instance) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 517a7093e81e..f374f82504a6 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -593,7 +593,7 @@ def test_clear_circuit(self): qc.clear() self.assertEqual(len(qc.data), 0) - self.assertEqual(qc._data.num_params(), 0) + self.assertEqual(qc._data.num_parameters(), 0) def test_barrier(self): """Test multiple argument forms of barrier.""" diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index ad336a0ad6bb..2261d903a63c 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -54,7 +54,9 @@ def raise_if_parameter_table_invalid(circuit): for parameter in param.parameters if isinstance(param, ParameterExpression) } - table_parameters = set(circuit._data.get_params_unsorted()) + if isinstance(circuit.global_phase, ParameterExpression): + circuit_parameters |= circuit.global_phase.parameters + table_parameters = set(circuit._data.unsorted_parameters()) if circuit_parameters != table_parameters: raise CircuitError( @@ -67,24 +69,31 @@ def raise_if_parameter_table_invalid(circuit): circuit_instructions = [instr.operation for instr in circuit._data] for parameter in table_parameters: - instr_list = circuit._data._get_param(parameter.uuid.int) + instr_list = circuit._data._raw_parameter_table_entry(parameter) for instr_index, param_index in instr_list: - instr = circuit.data[instr_index].operation - if instr not in circuit_instructions: - raise CircuitError(f"ParameterTable instruction not present in circuit: {instr}.") - - if not isinstance(instr.params[param_index], ParameterExpression): + if instr_index is None: + # Global phase. + expression = circuit.global_phase + instr = "" + else: + instr = circuit.data[instr_index].operation + if instr not in circuit_instructions: + raise CircuitError( + f"ParameterTable instruction not present in circuit: {instr}." + ) + expression = instr.params[param_index] + + if not isinstance(expression, ParameterExpression): raise CircuitError( "ParameterTable instruction does not have a " f"ParameterExpression at param_index {param_index}: {instr}." ) - if parameter not in instr.params[param_index].parameters: + if parameter not in expression.parameters: raise CircuitError( - "ParameterTable instruction parameters does " - "not match ParameterTable key. Instruction " - f"parameters: {instr.params[param_index].parameters}" - f" ParameterTable key: {parameter}." + "ParameterTable instruction parameters does not match ParameterTable key." + f"\nInstruction parameters: {expression.parameters}" + f"\nParameterTable key: {parameter}." ) # Assert circuit has no other parameter locations other than those in table. @@ -94,8 +103,8 @@ def raise_if_parameter_table_invalid(circuit): parameters = param.parameters for parameter in parameters: - if (instr_index, param_index) not in circuit._data._get_param( - parameter.uuid.int + if (instr_index, param_index) not in circuit._data._raw_parameter_table_entry( + parameter ): raise CircuitError( "Found parameterized instruction not " @@ -183,9 +192,10 @@ def test_parameters_property(self): qc = QuantumCircuit(qr) rxg = RXGate(theta) qc.append(rxg, [qr[0]], []) - self.assertEqual(qc._data.num_params(), 1) - self.assertIs(theta, next(iter(qc._data.get_params_unsorted()))) - self.assertEqual(rxg, qc.data[next(iter(qc._data._get_param(theta.uuid.int)))[0]].operation) + self.assertEqual(qc._data.num_parameters(), 1) + self.assertIs(theta, next(iter(qc._data.unsorted_parameters()))) + ((instruction_index, _),) = list(qc._data._raw_parameter_table_entry(theta)) + self.assertEqual(rxg, qc.data[instruction_index].operation) def test_parameters_property_by_index(self): """Test getting parameters by index""" @@ -581,12 +591,12 @@ def test_two_parameter_expression_binding(self): qc.rx(theta, 0) qc.ry(phi, 0) - self.assertEqual(qc._data._get_entry_count(theta), 1) - self.assertEqual(qc._data._get_entry_count(phi), 1) + self.assertEqual(len(qc._data._raw_parameter_table_entry(theta)), 1) + self.assertEqual(len(qc._data._raw_parameter_table_entry(phi)), 1) qc.assign_parameters({theta: -phi}, inplace=True) - self.assertEqual(qc._data._get_entry_count(phi), 2) + self.assertEqual(len(qc._data._raw_parameter_table_entry(phi)), 2) def test_expression_partial_binding_zero(self): """Verify that binding remains possible even if a previous partial bind @@ -610,6 +620,47 @@ def test_expression_partial_binding_zero(self): self.assertEqual(fbqc.parameters, set()) self.assertEqual(float(fbqc.data[0].operation.params[0]), 0) + def test_assignment_to_annotated_operation(self): + """Test that assignments to an ``AnnotatedOperation`` are propagated all the way down.""" + + class MyGate(Gate): + """Arbitrary non-standard gate.""" + + def __init__(self, param): + super().__init__("my_gate", 1, [param]) + # Eagerly create our definition. + _ = self.definition + + def _define(self): + self._definition = QuantumCircuit(1, name="my_gate_inner") + self._definition.ry(self.params[0], 0) + + theta = Parameter("theta") + + # Sanity check for the test; it won't catch errors if this fails. + self.assertEqual(MyGate(theta), MyGate(theta)) + self.assertNotEqual(MyGate(theta), MyGate(1.23)) + + parametric_gate = MyGate(theta) + qc = QuantumCircuit(2) + qc.append(parametric_gate.control(1, annotated=True), [0, 1], copy=True) + assigned = qc.assign_parameters([1.23]) + + expected = QuantumCircuit(2) + expected.append(MyGate(1.23).control(1, annotated=True), [0, 1]) + self.assertEqual(assigned, expected) + self.assertEqual( + assigned.data[0].operation.base_op.definition, + expected.data[0].operation.base_op.definition, + ) + + qc.assign_parameters([1.23], inplace=True) + self.assertEqual(qc, expected) + + # Test that the underlying gate was not modified. + self.assertEqual(parametric_gate.params, [theta]) + self.assertEqual(set(parametric_gate.definition.parameters), {theta}) + def test_raise_if_assigning_params_not_in_circuit(self): """Verify binding parameters which are not present in the circuit raises an error.""" x = Parameter("x") @@ -641,7 +692,7 @@ def test_gate_multiplicity_binding(self): qc.append(gate, [0], []) qc.append(gate, [0], []) qc2 = qc.assign_parameters({theta: 1.0}) - self.assertEqual(qc2._data.num_params(), 0) + self.assertEqual(qc2._data.num_parameters(), 0) for instruction in qc2.data: self.assertEqual(float(instruction.operation.params[0]), 1.0) @@ -1384,6 +1435,18 @@ def test_copy_after_dot_data_setter(self): self.assertEqual(qc.parameters, set()) raise_if_parameter_table_invalid(qc) + def test_nonfinal_insert_maintains_valid_table(self): + """Inserts other than appends should still maintain valid tracking, for as long as we + continue to allow non-final inserts.""" + a, b, c = [Parameter(x) for x in "abc"] + qc = QuantumCircuit(1) + qc.global_phase = a / 2 + qc.rz(a, 0) + qc.rz(b + c, 0) + raise_if_parameter_table_invalid(qc) + qc.data.insert(0, qc.data.pop()) + raise_if_parameter_table_invalid(qc) + def test_circuit_with_ufunc(self): """Test construction of circuit and binding of parameters after we apply universal functions."""