From 38ad0d106a6f10bf2bd09f9643c0fbb78f3f8446 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Sun, 28 Apr 2024 15:36:53 -0400 Subject: [PATCH 001/159] Removing the superfluous-parens lint rule and updates (#12303) --- pyproject.toml | 1 - qiskit/quantum_info/operators/symplectic/random.py | 2 +- test/python/quantum_info/states/test_utils.py | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40c6701fd05..975bd377760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,7 +225,6 @@ disable = [ "no-member", "no-value-for-parameter", "not-context-manager", - "superfluous-parens", "unexpected-keyword-arg", "unnecessary-dict-index-lookup", "unnecessary-dunder-call", diff --git a/qiskit/quantum_info/operators/symplectic/random.py b/qiskit/quantum_info/operators/symplectic/random.py index 1a845100b91..f9bd65ef918 100644 --- a/qiskit/quantum_info/operators/symplectic/random.py +++ b/qiskit/quantum_info/operators/symplectic/random.py @@ -81,7 +81,7 @@ def random_pauli_list( z = rng.integers(2, size=(size, num_qubits)).astype(bool) x = rng.integers(2, size=(size, num_qubits)).astype(bool) if phase: - _phase = rng.integers(4, size=(size)) + _phase = rng.integers(4, size=size) return PauliList.from_symplectic(z, x, _phase) return PauliList.from_symplectic(z, x) diff --git a/test/python/quantum_info/states/test_utils.py b/test/python/quantum_info/states/test_utils.py index 9a9015944e7..1382963ed55 100644 --- a/test/python/quantum_info/states/test_utils.py +++ b/test/python/quantum_info/states/test_utils.py @@ -113,14 +113,14 @@ def test_schmidt_decomposition_3_level_system(self): # check decomposition elements self.assertAlmostEqual(schmidt_comps[0][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[0][1], Statevector(np.array([1, 0, 0]), dims=(3))) - self.assertEqual(schmidt_comps[0][2], Statevector(np.array([1, 0, 0]), dims=(3))) + self.assertEqual(schmidt_comps[0][1], Statevector(np.array([1, 0, 0]), dims=3)) + self.assertEqual(schmidt_comps[0][2], Statevector(np.array([1, 0, 0]), dims=3)) self.assertAlmostEqual(schmidt_comps[1][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[1][1], Statevector(np.array([0, 1, 0]), dims=(3))) - self.assertEqual(schmidt_comps[1][2], Statevector(np.array([0, 1, 0]), dims=(3))) + self.assertEqual(schmidt_comps[1][1], Statevector(np.array([0, 1, 0]), dims=3)) + self.assertEqual(schmidt_comps[1][2], Statevector(np.array([0, 1, 0]), dims=3)) self.assertAlmostEqual(schmidt_comps[2][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[2][1], Statevector(np.array([0, 0, 1]), dims=(3))) - self.assertEqual(schmidt_comps[2][2], Statevector(np.array([0, 0, 1]), dims=(3))) + self.assertEqual(schmidt_comps[2][1], Statevector(np.array([0, 0, 1]), dims=3)) + self.assertEqual(schmidt_comps[2][2], Statevector(np.array([0, 0, 1]), dims=3)) # check that state can be properly reconstructed state = Statevector( From 8194a68b69eed99be7735e3a699bdcf33250a7f0 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:13:42 +0300 Subject: [PATCH 002/159] Add reverse permutation for LNN connectivity (#12181) * add permutation reverse lnn function * update init files * update other synthesis functions * add tests * add release notes * add helper function _append_reverse_permutation_lnn_kms * add more cases to test --- qiskit/synthesis/__init__.py | 2 + qiskit/synthesis/linear_phase/cz_depth_lnn.py | 22 +---- qiskit/synthesis/permutation/__init__.py | 1 + .../permutation/permutation_reverse_lnn.py | 90 +++++++++++++++++++ qiskit/synthesis/qft/qft_decompose_lnn.py | 8 +- ...erse-permutation-lnn-409a07c7f6d0eed9.yaml | 8 ++ .../synthesis/test_permutation_synthesis.py | 28 +++++- 7 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 qiskit/synthesis/permutation/permutation_reverse_lnn.py create mode 100644 releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index c191eac9747..49e7885d509 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -51,6 +51,7 @@ .. autofunction:: synth_permutation_depth_lnn_kms .. autofunction:: synth_permutation_basic .. autofunction:: synth_permutation_acg +.. autofunction:: synth_permutation_reverse_lnn_kms Clifford Synthesis ================== @@ -140,6 +141,7 @@ synth_permutation_depth_lnn_kms, synth_permutation_basic, synth_permutation_acg, + synth_permutation_reverse_lnn_kms, ) from .linear import ( synth_cnot_count_full_pmh, diff --git a/qiskit/synthesis/linear_phase/cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cz_depth_lnn.py index b3931d07817..6dc7db5d619 100644 --- a/qiskit/synthesis/linear_phase/cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cz_depth_lnn.py @@ -24,24 +24,10 @@ import numpy as np from qiskit.circuit import QuantumCircuit - - -def _append_cx_stage1(qc, n): - """A single layer of CX gates.""" - for i in range(n // 2): - qc.cx(2 * i, 2 * i + 1) - for i in range((n + 1) // 2 - 1): - qc.cx(2 * i + 2, 2 * i + 1) - return qc - - -def _append_cx_stage2(qc, n): - """A single layer of CX gates.""" - for i in range(n // 2): - qc.cx(2 * i + 1, 2 * i) - for i in range((n + 1) // 2 - 1): - qc.cx(2 * i + 1, 2 * i + 2) - return qc +from qiskit.synthesis.permutation.permutation_reverse_lnn import ( + _append_cx_stage1, + _append_cx_stage2, +) def _odd_pattern1(n): diff --git a/qiskit/synthesis/permutation/__init__.py b/qiskit/synthesis/permutation/__init__.py index 7cc8d0174d7..5a8b9a7a13f 100644 --- a/qiskit/synthesis/permutation/__init__.py +++ b/qiskit/synthesis/permutation/__init__.py @@ -15,3 +15,4 @@ from .permutation_lnn import synth_permutation_depth_lnn_kms from .permutation_full import synth_permutation_basic, synth_permutation_acg +from .permutation_reverse_lnn import synth_permutation_reverse_lnn_kms diff --git a/qiskit/synthesis/permutation/permutation_reverse_lnn.py b/qiskit/synthesis/permutation/permutation_reverse_lnn.py new file mode 100644 index 00000000000..26287a06177 --- /dev/null +++ b/qiskit/synthesis/permutation/permutation_reverse_lnn.py @@ -0,0 +1,90 @@ +# 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. +""" +Synthesis of a reverse permutation for LNN connectivity. +""" + +from qiskit.circuit import QuantumCircuit + + +def _append_cx_stage1(qc, n): + """A single layer of CX gates.""" + for i in range(n // 2): + qc.cx(2 * i, 2 * i + 1) + for i in range((n + 1) // 2 - 1): + qc.cx(2 * i + 2, 2 * i + 1) + return qc + + +def _append_cx_stage2(qc, n): + """A single layer of CX gates.""" + for i in range(n // 2): + qc.cx(2 * i + 1, 2 * i) + for i in range((n + 1) // 2 - 1): + qc.cx(2 * i + 1, 2 * i + 2) + return qc + + +def _append_reverse_permutation_lnn_kms(qc: QuantumCircuit, num_qubits: int) -> None: + """ + Append reverse permutation to a QuantumCircuit for linear nearest-neighbor architectures + using Kutin, Moulton, Smithline method. + + Synthesis algorithm for reverse permutation from [1], section 5. + This algorithm synthesizes the reverse permutation on :math:`n` qubits over + a linear nearest-neighbor architecture using CX gates with depth :math:`2 * n + 2`. + + Args: + qc: The original quantum circuit. + num_qubits: The number of qubits. + + Returns: + The quantum circuit with appended reverse permutation. + + References: + 1. Kutin, S., Moulton, D. P., Smithline, L., + *Computation at a distance*, Chicago J. Theor. Comput. Sci., vol. 2007, (2007), + `arXiv:quant-ph/0701194 `_ + """ + + for _ in range((num_qubits + 1) // 2): + _append_cx_stage1(qc, num_qubits) + _append_cx_stage2(qc, num_qubits) + if (num_qubits % 2) == 0: + _append_cx_stage1(qc, num_qubits) + + +def synth_permutation_reverse_lnn_kms(num_qubits: int) -> QuantumCircuit: + """ + Synthesize reverse permutation for linear nearest-neighbor architectures using + Kutin, Moulton, Smithline method. + + Synthesis algorithm for reverse permutation from [1], section 5. + This algorithm synthesizes the reverse permutation on :math:`n` qubits over + a linear nearest-neighbor architecture using CX gates with depth :math:`2 * n + 2`. + + Args: + num_qubits: The number of qubits. + + Returns: + The synthesized quantum circuit. + + References: + 1. Kutin, S., Moulton, D. P., Smithline, L., + *Computation at a distance*, Chicago J. Theor. Comput. Sci., vol. 2007, (2007), + `arXiv:quant-ph/0701194 `_ + """ + + qc = QuantumCircuit(num_qubits) + _append_reverse_permutation_lnn_kms(qc, num_qubits) + + return qc diff --git a/qiskit/synthesis/qft/qft_decompose_lnn.py b/qiskit/synthesis/qft/qft_decompose_lnn.py index 4dd8d9d56d1..a54be481f51 100644 --- a/qiskit/synthesis/qft/qft_decompose_lnn.py +++ b/qiskit/synthesis/qft/qft_decompose_lnn.py @@ -15,7 +15,7 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.synthesis.linear_phase.cz_depth_lnn import _append_cx_stage1, _append_cx_stage2 +from qiskit.synthesis.permutation.permutation_reverse_lnn import _append_reverse_permutation_lnn_kms def synth_qft_line( @@ -65,10 +65,6 @@ def synth_qft_line( if not do_swaps: # Add a reversal network for LNN connectivity in depth 2*n+2, # based on Kutin at al., https://arxiv.org/abs/quant-ph/0701194, Section 5. - for _ in range((num_qubits + 1) // 2): - qc = _append_cx_stage1(qc, num_qubits) - qc = _append_cx_stage2(qc, num_qubits) - if (num_qubits % 2) == 0: - qc = _append_cx_stage1(qc, num_qubits) + _append_reverse_permutation_lnn_kms(qc, num_qubits) return qc diff --git a/releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml b/releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml new file mode 100644 index 00000000000..357345adfa2 --- /dev/null +++ b/releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml @@ -0,0 +1,8 @@ +--- +features_synthesis: + - | + Add a new synthesis method :func:`.synth_permutation_reverse_lnn_kms` + of reverse permutations for linear nearest-neighbor architectures using + Kutin, Moulton, Smithline method. + This algorithm synthesizes the reverse permutation on :math:`n` qubits over + a linear nearest-neighbor architecture using CX gates with depth :math:`2 * n + 2`. diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 7fc6f5e24ab..5c4317ed58a 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -19,8 +19,12 @@ from qiskit.quantum_info.operators import Operator from qiskit.circuit.library import LinearFunction, PermutationGate -from qiskit.synthesis import synth_permutation_acg -from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms, synth_permutation_basic +from qiskit.synthesis.permutation import ( + synth_permutation_acg, + synth_permutation_depth_lnn_kms, + synth_permutation_basic, + synth_permutation_reverse_lnn_kms, +) from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -108,6 +112,26 @@ def test_synth_permutation_depth_lnn_kms(self, width): synthesized_pattern = LinearFunction(qc).permutation_pattern() self.assertTrue(np.array_equal(synthesized_pattern, pattern)) + @data(1, 2, 3, 4, 5, 10, 15, 20) + def test_synth_permutation_reverse_lnn_kms(self, num_qubits): + """Test synth_permutation_reverse_lnn_kms function produces the correct + circuit.""" + pattern = list(reversed(range(num_qubits))) + qc = synth_permutation_reverse_lnn_kms(num_qubits) + self.assertListEqual((LinearFunction(qc).permutation_pattern()).tolist(), pattern) + + # Check that the CX depth of the circuit is at 2*n+2 + self.assertTrue(qc.depth() <= 2 * num_qubits + 2) + + # Check that the synthesized circuit consists of CX gates only, + # and that these CXs adhere to the LNN connectivity. + for instruction in qc.data: + self.assertEqual(instruction.operation.name, "cx") + q0 = qc.find_bit(instruction.qubits[0]).index + q1 = qc.find_bit(instruction.qubits[1]).index + dist = abs(q0 - q1) + self.assertEqual(dist, 1) + @data(4, 5, 6, 7) def test_permutation_matrix(self, width): """Test that the unitary matrix constructed from permutation pattern From 393524f0abc39f57108c26f716617f7ce4005d99 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 Apr 2024 17:28:37 +0200 Subject: [PATCH 003/159] Rework handling of instruction durations in preset pass managers (#12183) * Rework use of instruction durations, move logic from transpile function to individual passes. * Apply review feedback on reno --- qiskit/compiler/transpiler.py | 78 +++---------------- .../passes/scheduling/dynamical_decoupling.py | 27 ++++++- .../padding/dynamical_decoupling.py | 26 ++++++- .../passes/scheduling/time_unit_conversion.py | 32 +++++++- ...nst-durations-passes-28c78401682e22c0.yaml | 15 ++++ 5 files changed, 103 insertions(+), 75 deletions(-) create mode 100644 releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 5514bb168fa..95c583ceeaa 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -337,7 +337,7 @@ def callback_func(**kwargs): _skip_target = False _given_inst_map = bool(inst_map) # check before inst_map is overwritten - # If a target is specified have it override any implicit selections from a backend + # If a target is specified, have it override any implicit selections from a backend if target is not None: if coupling_map is None: coupling_map = target.build_coupling_map() @@ -354,7 +354,7 @@ def callback_func(**kwargs): if backend_properties is None: backend_properties = target_to_backend_properties(target) # If target is not specified and any hardware constraint object is - # manually specified then do not use the target from the backend as + # manually specified, do not use the target from the backend as # it is invalidated by a custom basis gate list, custom coupling map, # custom dt or custom instruction_durations elif ( @@ -379,6 +379,7 @@ def callback_func(**kwargs): _check_circuits_coupling_map(circuits, coupling_map, backend) timing_constraints = _parse_timing_constraints(backend, timing_constraints) + instruction_durations = _parse_instruction_durations(backend, instruction_durations, dt) if _given_inst_map and inst_map.has_custom_gate() and target is not None: # Do not mutate backend target @@ -391,51 +392,6 @@ def callback_func(**kwargs): if translation_method is None and hasattr(backend, "get_translation_stage_plugin"): translation_method = backend.get_translation_stage_plugin() - if instruction_durations or dt: - # If durations are provided and there is more than one circuit - # we need to serialize the execution because the full durations - # is dependent on the circuit calibrations which are per circuit - if len(circuits) > 1: - out_circuits = [] - for circuit in circuits: - instruction_durations = _parse_instruction_durations( - backend, instruction_durations, dt, circuit - ) - pm = generate_preset_pass_manager( - optimization_level, - backend=backend, - target=target, - basis_gates=basis_gates, - inst_map=inst_map, - coupling_map=coupling_map, - instruction_durations=instruction_durations, - backend_properties=backend_properties, - timing_constraints=timing_constraints, - initial_layout=initial_layout, - layout_method=layout_method, - routing_method=routing_method, - translation_method=translation_method, - scheduling_method=scheduling_method, - approximation_degree=approximation_degree, - seed_transpiler=seed_transpiler, - unitary_synthesis_method=unitary_synthesis_method, - unitary_synthesis_plugin_config=unitary_synthesis_plugin_config, - hls_config=hls_config, - init_method=init_method, - optimization_method=optimization_method, - _skip_target=_skip_target, - ) - out_circuits.append(pm.run(circuit, callback=callback, num_processes=num_processes)) - for name, circ in zip(output_name, out_circuits): - circ.name = name - end_time = time() - _log_transpile_time(start_time, end_time) - return out_circuits - else: - instruction_durations = _parse_instruction_durations( - backend, instruction_durations, dt, circuits[0] - ) - pm = generate_preset_pass_manager( optimization_level, backend=backend, @@ -460,7 +416,7 @@ def callback_func(**kwargs): optimization_method=optimization_method, _skip_target=_skip_target, ) - out_circuits = pm.run(circuits, callback=callback) + out_circuits = pm.run(circuits, callback=callback, num_processes=num_processes) for name, circ in zip(output_name, out_circuits): circ.name = name end_time = time() @@ -535,32 +491,20 @@ def _parse_initial_layout(initial_layout): return initial_layout -def _parse_instruction_durations(backend, inst_durations, dt, circuit): +def _parse_instruction_durations(backend, inst_durations, dt): """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided, the backend will be ignored, otherwise, the durations will be populated from the - backend. If any circuits have gate calibrations, those calibration durations would - take precedence over backend durations, but be superceded by ``inst_duration``s. + backend. """ + final_durations = InstructionDurations() if not inst_durations: backend_durations = InstructionDurations() if backend is not None: backend_durations = backend.instruction_durations - - circ_durations = InstructionDurations() - if not inst_durations: - circ_durations.update(backend_durations, dt or backend_durations.dt) - - if circuit.calibrations: - cal_durations = [] - for gate, gate_cals in circuit.calibrations.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) - circ_durations.update(cal_durations, circ_durations.dt) - - if inst_durations: - circ_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) - - return circ_durations + final_durations.update(backend_durations, dt or backend_durations.dt) + else: + final_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) + return final_durations def _parse_approximation_degree(approximation_degree): diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 5b84b529e45..12f4bc515b2 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -20,6 +20,7 @@ from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.synthesis.one_qubit import OneQubitEulerDecomposer +from qiskit.transpiler import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -168,6 +169,8 @@ def run(self, dag): if dag.duration is None: raise TranspilerError("DD runs after circuit is scheduled.") + durations = self._update_inst_durations(dag) + num_pulses = len(self._dd_sequence) sequence_gphase = 0 if num_pulses != 1: @@ -208,7 +211,7 @@ def run(self, dag): for index, gate in enumerate(self._dd_sequence): gate = gate.to_mutable() self._dd_sequence[index] = gate - gate.duration = self._durations.get(gate, physical_qubit) + gate.duration = durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration @@ -277,6 +280,26 @@ def run(self, dag): return new_dag + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations is not None: + circ_durations.update(self._durations, getattr(self._durations, "dt", None)) + + return circ_durations + def __gate_supported(self, gate: Gate, qarg: int) -> bool: """A gate is supported on the qubit (qarg) or not.""" if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 42a1bdc80f1..7cb309dd9aa 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -179,9 +179,31 @@ def __init__( f"{gate.name} in dd_sequence is not supported in the target" ) + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations is not None: + circ_durations.update(self._durations, getattr(self._durations, "dt", None)) + + return circ_durations + def _pre_runhook(self, dag: DAGCircuit): super()._pre_runhook(dag) + durations = self._update_inst_durations(dag) + num_pulses = len(self._dd_sequence) # Check if physical circuit is given @@ -245,7 +267,7 @@ def _pre_runhook(self, dag: DAGCircuit): f"is not acceptable in {self.__class__.__name__} pass." ) except KeyError: - gate_length = self._durations.get(gate, physical_index) + gate_length = durations.get(gate, physical_index) sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. gate = gate.to_mutable() diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index d53c3fc4ef6..25672c137f3 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -51,6 +51,7 @@ def __init__(self, inst_durations: InstructionDurations = None, target: Target = self.inst_durations = inst_durations or InstructionDurations() if target is not None: self.inst_durations = target.durations() + self._durations_provided = inst_durations is not None or target is not None def run(self, dag: DAGCircuit): """Run the TimeUnitAnalysis pass on `dag`. @@ -64,8 +65,11 @@ def run(self, dag: DAGCircuit): Raises: TranspilerError: if the units are not unifiable """ + + inst_durations = self._update_inst_durations(dag) + # Choose unit - if self.inst_durations.dt is not None: + if inst_durations.dt is not None: time_unit = "dt" else: # Check what units are used in delays and other instructions: dt or SI or mixed @@ -75,7 +79,7 @@ def run(self, dag: DAGCircuit): "Fail to unify time units in delays. SI units " "and dt unit must not be mixed when dt is not supplied." ) - units_other = self.inst_durations.units_used() + units_other = inst_durations.units_used() if self._unified(units_other) == "mixed": raise TranspilerError( "Fail to unify time units in instruction_durations. SI units " @@ -96,7 +100,7 @@ def run(self, dag: DAGCircuit): # Make units consistent for node in dag.op_nodes(): try: - duration = self.inst_durations.get( + duration = inst_durations.get( node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit ) except TranspilerError: @@ -108,6 +112,26 @@ def run(self, dag: DAGCircuit): self.property_set["time_unit"] = time_unit return dag + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations_provided: + circ_durations.update(self.inst_durations, getattr(self.inst_durations, "dt", None)) + + return circ_durations + @staticmethod def _units_used_in_delays(dag: DAGCircuit) -> Set[str]: units_used = set() diff --git a/releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml b/releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml new file mode 100644 index 00000000000..2ccd92f19c1 --- /dev/null +++ b/releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + The internal handling of custom circuit calibrations and :class:`.InstructionDurations` + has been offloaded from the :func:`.transpile` function to the individual transpiler passes: + :class:`qiskit.transpiler.passes.scheduling.DynamicalDecoupling`, + :class:`qiskit.transpiler.passes.scheduling.padding.DynamicalDecoupling`. Before, + instruction durations from circuit calibrations would not be taken into account unless + they were manually incorporated into `instruction_durations` input argument, but the passes + that need it now analyze the circuit and pick the most relevant duration value according + to the following priority order: target > custom input > circuit calibrations. + + - | + Fixed a bug in :func:`.transpile` where the ``num_processes`` argument would only be used + if ``dt`` or ``instruction_durations`` were provided. \ No newline at end of file From b442b1c4fd68628f140ba5dd38e8d1a6577f2eeb Mon Sep 17 00:00:00 2001 From: Arthur Strauss <56998701+arthurostrauss@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:34:25 +0200 Subject: [PATCH 004/159] Assignment of parameters in pulse `Schedule`/`ScheduleBlock` doable through parameter name (#12088) * Added possibility of assigning Parameter and ParameterVector by name It is now possible to specify in the mapping the names of the parameters instead of the parameters themselves to assign parameters to pulse `Schedule`s and `ScheduleBlock`s. It is even possible to assign all the parameters of a ParameterVector by just specifying its name and setting as a value a list of parameter values. * Update parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml Co-authored-by: Will Shanks * Reshaped string parameter assignment Corrected mistake in string assignment Corrected mistake in string assignment * Update releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml Co-authored-by: Will Shanks * Enabled string assignment for multiple params carrying same name The check is now based on the value type to infer if assignment should be done on Parameters or ParameterVectors Removed unnecessary import from utils Corrected string assignment Correction part 2 Corrected test The inplace=True argument was preventing the reuse of a parametrized waveform in the schedule, making the test fail * Reformat --------- Co-authored-by: Will Shanks --- qiskit/pulse/parameter_manager.py | 45 ++++++++++++------- qiskit/pulse/schedule.py | 16 ++++--- qiskit/pulse/utils.py | 35 ++++++++++++++- ..._for_pulse_schedules-3a27bbbbf235fb9e.yaml | 8 ++++ test/python/pulse/test_parameter_manager.py | 38 ++++++++++++++++ 5 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 561eac01f55..e5a4a1a1d2b 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -54,7 +54,7 @@ from copy import copy from typing import Any, Mapping, Sequence -from qiskit.circuit import ParameterVector +from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse import instructions, channels @@ -62,7 +62,11 @@ from qiskit.pulse.library import SymbolicPulse, Waveform from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind -from qiskit.pulse.utils import format_parameter_value +from qiskit.pulse.utils import ( + format_parameter_value, + _validate_parameter_vector, + _validate_parameter_value, +) class NodeVisitor: @@ -362,7 +366,8 @@ def assign_parameters( self, pulse_program: Any, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], ) -> Any: """Modify and return program data with parameters assigned according to the input. @@ -397,7 +402,7 @@ def update_parameter_table(self, new_node: Any): def _unroll_param_dict( self, parameter_binds: Mapping[ - Parameter | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + Parameter | ParameterVector | str, ParameterValueType | Sequence[ParameterValueType] ], ) -> Mapping[Parameter, ParameterValueType]: """ @@ -410,21 +415,31 @@ def _unroll_param_dict( A dictionary from parameter to value. """ out = {} + param_name_dict = {param.name: [] for param in self.parameters} + for param in self.parameters: + param_name_dict[param.name].append(param) + param_vec_dict = { + param.vector.name: param.vector + for param in self.parameters + if isinstance(param, ParameterVectorElement) + } + for name in param_vec_dict.keys(): + if name in param_name_dict: + param_name_dict[name].append(param_vec_dict[name]) + else: + param_name_dict[name] = [param_vec_dict[name]] + for parameter, value in parameter_binds.items(): if isinstance(parameter, ParameterVector): - if not isinstance(value, Sequence): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to a single value." - ) - if len(parameter) != len(value): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to {len(value)} values." - ) + _validate_parameter_vector(parameter, value) out.update(zip(parameter, value)) elif isinstance(parameter, str): - out[self.get_parameters(parameter)] = value + for param in param_name_dict[parameter]: + is_vec = _validate_parameter_value(param, value) + if is_vec: + out.update(zip(param, value)) + else: + out[param] = value else: out[parameter] = value return out diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index a4d1ad844e5..5241da0c31d 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -715,16 +715,17 @@ def is_parameterized(self) -> bool: def assign_parameters( self, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], inplace: bool = True, ) -> "Schedule": """Assign the parameters in this schedule according to the input. Args: - value_dict: A mapping from parameters (parameter vectors) to either - numeric values (list of numeric values) - or another Parameter expression (list of Parameter expressions). + value_dict: A mapping from parameters or parameter names (parameter vector + or parameter vector name) to either numeric values (list of numeric values) + or another parameter expression (list of parameter expressions). inplace: Set ``True`` to override this instance with new parameter. Returns: @@ -1416,15 +1417,16 @@ def is_referenced(self) -> bool: def assign_parameters( self, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], inplace: bool = True, ) -> "ScheduleBlock": """Assign the parameters in this schedule according to the input. Args: - value_dict: A mapping from parameters (parameter vectors) to either numeric values - (list of numeric values) + value_dict: A mapping from parameters or parameter names (parameter vector + or parameter vector name) to either numeric values (list of numeric values) or another parameter expression (list of parameter expressions). inplace: Set ``True`` to override this instance with new parameter. diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py index fddc9469add..ae87fbafadd 100644 --- a/qiskit/pulse/utils.py +++ b/qiskit/pulse/utils.py @@ -11,13 +11,14 @@ # that they have been altered from the originals. """Module for common pulse programming utilities.""" -from typing import List, Dict, Union +from typing import List, Dict, Union, Sequence import warnings import numpy as np +from qiskit.circuit import ParameterVector, Parameter from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError +from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError, PulseError def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]: @@ -117,3 +118,33 @@ def instruction_duration_validation(duration: int): raise QiskitError( f"Instruction duration must be a non-negative integer, got {duration} instead." ) + + +def _validate_parameter_vector(parameter: ParameterVector, value): + """Validate parameter vector and its value.""" + if not isinstance(value, Sequence): + raise PulseError( + f"Parameter vector '{parameter.name}' has length {len(parameter)}," + f" but was assigned to {value}." + ) + if len(parameter) != len(value): + raise PulseError( + f"Parameter vector '{parameter.name}' has length {len(parameter)}," + f" but was assigned to {len(value)} values." + ) + + +def _validate_single_parameter(parameter: Parameter, value): + """Validate single parameter and its value.""" + if not isinstance(value, (int, float, complex, ParameterExpression)): + raise PulseError(f"Parameter '{parameter.name}' is not assignable to {value}.") + + +def _validate_parameter_value(parameter, value): + """Validate parameter and its value.""" + if isinstance(parameter, ParameterVector): + _validate_parameter_vector(parameter, value) + return True + else: + _validate_single_parameter(parameter, value) + return False diff --git a/releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml b/releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml new file mode 100644 index 00000000000..551ea9e918c --- /dev/null +++ b/releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml @@ -0,0 +1,8 @@ +--- +features_pulse: + - | + It is now possible to assign parameters to pulse :class:`.Schedule`and :class:`.ScheduleBlock` objects by specifying + the parameter name as a string. The parameter name can be used to assign values to all parameters within the + `Schedule` or `ScheduleBlock` that have the same name. Moreover, the parameter name of a `ParameterVector` + can be used to assign all values of the vector simultaneously (the list of values should therefore match the + length of the vector). diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index 54268af1457..0b91aaeaab4 100644 --- a/test/python/pulse/test_parameter_manager.py +++ b/test/python/pulse/test_parameter_manager.py @@ -515,6 +515,44 @@ def test_parametric_pulses_with_parameter_vector(self): self.assertEqual(sched2.instructions[0][1].pulse.sigma, 4.0) self.assertEqual(sched2.instructions[1][1].phase, 0.1) + def test_pulse_assignment_with_parameter_names(self): + """Test pulse assignment with parameter names.""" + sigma = Parameter("sigma") + amp = Parameter("amp") + param_vec = ParameterVector("param_vec", 2) + + waveform = pulse.library.Gaussian(duration=128, sigma=sigma, amp=amp) + waveform2 = pulse.library.Gaussian(duration=128, sigma=40, amp=amp) + block = pulse.ScheduleBlock() + block += pulse.Play(waveform, pulse.DriveChannel(10)) + block += pulse.Play(waveform2, pulse.DriveChannel(10)) + block += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) + block += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) + block1 = block.assign_parameters( + {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False + ) + + self.assertEqual(block1.blocks[0].pulse.amp, 0.2) + self.assertEqual(block1.blocks[0].pulse.sigma, 4.0) + self.assertEqual(block1.blocks[1].pulse.amp, 0.2) + self.assertEqual(block1.blocks[2].phase, 3.14) + self.assertEqual(block1.blocks[3].phase, 1.57) + + sched = pulse.Schedule() + sched += pulse.Play(waveform, pulse.DriveChannel(10)) + sched += pulse.Play(waveform2, pulse.DriveChannel(10)) + sched += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) + sched += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) + sched1 = sched.assign_parameters( + {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False + ) + + self.assertEqual(sched1.instructions[0][1].pulse.amp, 0.2) + self.assertEqual(sched1.instructions[0][1].pulse.sigma, 4.0) + self.assertEqual(sched1.instructions[1][1].pulse.amp, 0.2) + self.assertEqual(sched1.instructions[2][1].phase, 3.14) + self.assertEqual(sched1.instructions[3][1].phase, 1.57) + class TestScheduleTimeslots(QiskitTestCase): """Test for edge cases of timing overlap on parametrized channels. From aacd4935f4cacfdabaa1179b0fe90496148f658e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Apr 2024 03:07:31 +0100 Subject: [PATCH 005/159] Have `QuantumCircuit.store` do integer-literal type promotion (#12201) * Have `QuantumCircuit.store` do integer-literal type promotion `QuantumCircuit.store` does convenience lifting of arbitary values to `expr.Expr` nodes. Similar to the binary-operation creators, it's convenient to have `QuantumCircuit.store` correctly infer the required widths of integer literals in the rvalue. This isn't full type inference, just a small amount of convenience. * Add integer-literal widening to `add_var` * Add no-widen test of `QuantumCircuit.add_var` --- qiskit/circuit/quantumcircuit.py | 21 +++++++++++++++++-- test/python/circuit/test_circuit_vars.py | 26 +++++++++++++++++++++++- test/python/circuit/test_store.py | 16 +++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index f25dca4b03b..b19269a4916 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1765,7 +1765,18 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # Validate the initialiser first to catch cases where the variable to be declared is being # used in the initialiser. circuit_scope = self._current_scope() - initial = _validate_expr(circuit_scope, expr.lift(initial)) + # Convenience method to widen Python integer literals to the right width during the initial + # lift, if the type is already known via the variable. + if ( + isinstance(name_or_var, expr.Var) + and name_or_var.type.kind is types.Uint + and isinstance(initial, int) + and not isinstance(initial, bool) + ): + coerce_type = name_or_var.type + else: + coerce_type = None + initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) elif not name_or_var.standalone: @@ -2669,7 +2680,13 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: :meth:`add_var` Create a new variable in the circuit that can be written to with this method. """ - return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), (), copy=False) + # As a convenience, lift integer-literal rvalues to the matching width. + lvalue = expr.lift(lvalue) + rvalue_type = ( + lvalue.type if isinstance(rvalue, int) and not isinstance(rvalue, bool) else None + ) + rvalue = expr.lift(rvalue, rvalue_type) + return self.append(Store(lvalue, rvalue), (), (), copy=False) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index 0da54108536..8b7167eed7e 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -14,7 +14,7 @@ from test import QiskitTestCase -from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister +from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister, Store from qiskit.circuit.classical import expr, types @@ -241,6 +241,30 @@ def test_initialise_declarations_equal_to_add_var(self): self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) self.assertEqual(qc_init.data, qc_manual.data) + def test_declarations_widen_integer_literals(self): + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(16)) + qc = QuantumCircuit(declarations=[(a, 3)]) + qc.add_var(b, 5) + actual_initializers = [ + (op.lvalue, op.rvalue) + for instruction in qc + if isinstance((op := instruction.operation), Store) + ] + expected_initializers = [ + (a, expr.Value(3, types.Uint(8))), + (b, expr.Value(5, types.Uint(16))), + ] + self.assertEqual(actual_initializers, expected_initializers) + + def test_declaration_does_not_widen_bool_literal(self): + # `bool` is a subclass of `int` in Python (except some arithmetic operations have different + # semantics...). It's not in Qiskit's value type system, though. + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "explicit cast is required"): + qc.add_var(a, True) + def test_cannot_shadow_vars(self): """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are detected and rejected.""" diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index b44aac51f7a..425eae55a4b 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -133,6 +133,22 @@ def test_lifts_values(self): qc.store(b, 0xFFFF) self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) + def test_lifts_integer_literals_to_full_width(self): + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + qc.store(a, 1) + self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(1, a.type))) + qc.store(a, 255) + self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(255, a.type))) + + def test_does_not_widen_bool_literal(self): + # `bool` is a subclass of `int` in Python (except some arithmetic operations have different + # semantics...). It's not in Qiskit's value type system, though. + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "explicit cast is required"): + qc.store(a, True) + def test_rejects_vars_not_in_circuit(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) From 7ec2c56c068a3eda8608fce04e648c1f95b1dc6f Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 30 Apr 2024 08:56:51 +0200 Subject: [PATCH 006/159] Extend the basis gates of BasicSimulator (#12186) * extend the basis gates of BasicSimulator * remove variational sized gates * reno and test * accidentally missing u2 and u1 * three qubits support * support for parameters * support for all the standard gates upto 3 qubits * test * adding rccx * missing gates * test notes * readjust test * check target * tt * dict lookup * reno --- qiskit/circuit/library/standard_gates/x.py | 14 +- .../basic_provider/basic_provider_tools.py | 98 +++- .../basic_provider/basic_simulator.py | 91 ++- .../notes/fixes_10852-e197344c5f44b4f1.yaml | 5 + .../basic_provider/test_standard_library.py | 531 ++++++++++++++++++ .../transpiler/test_passmanager_config.py | 106 ++-- 6 files changed, 757 insertions(+), 88 deletions(-) create mode 100644 releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml create mode 100644 test/python/providers/basic_provider/test_standard_library.py diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index c0eb505efba..7195df90dc9 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -107,7 +107,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -250,7 +250,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -444,7 +444,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -585,7 +585,7 @@ def __init__( Args: 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. """ from .sx import SXGate @@ -785,7 +785,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -1029,7 +1029,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -1204,7 +1204,7 @@ def control( 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. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. diff --git a/qiskit/providers/basic_provider/basic_provider_tools.py b/qiskit/providers/basic_provider/basic_provider_tools.py index 030c629275e..b2670cc0977 100644 --- a/qiskit/providers/basic_provider/basic_provider_tools.py +++ b/qiskit/providers/basic_provider/basic_provider_tools.py @@ -23,7 +23,30 @@ from qiskit.exceptions import QiskitError # Single qubit gates supported by ``single_gate_params``. -SINGLE_QUBIT_GATES = ("U", "u", "h", "p", "u1", "u2", "u3", "rz", "sx", "x") +SINGLE_QUBIT_GATES = { + "U": gates.UGate, + "u": gates.UGate, + "u1": gates.U1Gate, + "u2": gates.U2Gate, + "u3": gates.U3Gate, + "h": gates.HGate, + "p": gates.PhaseGate, + "s": gates.SGate, + "sdg": gates.SdgGate, + "sx": gates.SXGate, + "sxdg": gates.SXdgGate, + "t": gates.TGate, + "tdg": gates.TdgGate, + "x": gates.XGate, + "y": gates.YGate, + "z": gates.ZGate, + "id": gates.IGate, + "i": gates.IGate, + "r": gates.RGate, + "rx": gates.RXGate, + "ry": gates.RYGate, + "rz": gates.RZGate, +} def single_gate_matrix(gate: str, params: list[float] | None = None) -> np.ndarray: @@ -40,42 +63,55 @@ def single_gate_matrix(gate: str, params: list[float] | None = None) -> np.ndarr """ if params is None: params = [] - - if gate == "U": - gc = gates.UGate - elif gate == "u3": - gc = gates.U3Gate - elif gate == "h": - gc = gates.HGate - elif gate == "u": - gc = gates.UGate - elif gate == "p": - gc = gates.PhaseGate - elif gate == "u2": - gc = gates.U2Gate - elif gate == "u1": - gc = gates.U1Gate - elif gate == "rz": - gc = gates.RZGate - elif gate == "id": - gc = gates.IGate - elif gate == "sx": - gc = gates.SXGate - elif gate == "x": - gc = gates.XGate + if gate in SINGLE_QUBIT_GATES: + gc = SINGLE_QUBIT_GATES[gate] else: raise QiskitError("Gate is not a valid basis gate for this simulator: %s" % gate) return gc(*params).to_matrix() -# Cache CX matrix as no parameters. -_CX_MATRIX = gates.CXGate().to_matrix() - - -def cx_gate_matrix() -> np.ndarray: - """Get the matrix for a controlled-NOT gate.""" - return _CX_MATRIX +# Two qubit gates WITHOUT parameters: name -> matrix +TWO_QUBIT_GATES = { + "CX": gates.CXGate().to_matrix(), + "cx": gates.CXGate().to_matrix(), + "ecr": gates.ECRGate().to_matrix(), + "cy": gates.CYGate().to_matrix(), + "cz": gates.CZGate().to_matrix(), + "swap": gates.SwapGate().to_matrix(), + "iswap": gates.iSwapGate().to_matrix(), + "ch": gates.CHGate().to_matrix(), + "cs": gates.CSGate().to_matrix(), + "csdg": gates.CSdgGate().to_matrix(), + "csx": gates.CSXGate().to_matrix(), + "dcx": gates.DCXGate().to_matrix(), +} + +# Two qubit gates WITH parameters: name -> class +TWO_QUBIT_GATES_WITH_PARAMETERS = { + "cp": gates.CPhaseGate, + "crx": gates.CRXGate, + "cry": gates.CRYGate, + "crz": gates.CRZGate, + "cu": gates.CUGate, + "cu1": gates.CU1Gate, + "cu3": gates.CU3Gate, + "rxx": gates.RXXGate, + "ryy": gates.RYYGate, + "rzz": gates.RZZGate, + "rzx": gates.RZXGate, + "xx_minus_yy": gates.XXMinusYYGate, + "xx_plus_yy": gates.XXPlusYYGate, +} + + +# Three qubit gates: name -> matrix +THREE_QUBIT_GATES = { + "ccx": gates.CCXGate().to_matrix(), + "ccz": gates.CCZGate().to_matrix(), + "rccx": gates.RCCXGate().to_matrix(), + "cswap": gates.CSwapGate().to_matrix(), +} def einsum_matmul_index(gate_indices: list[int], number_of_qubits: int) -> str: diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index e1902151919..b03a8df7ae5 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -40,7 +40,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import UnitaryGate -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping, GlobalPhaseGate from qiskit.providers import Provider from qiskit.providers.backend import BackendV2 from qiskit.providers.models import BackendConfiguration @@ -51,8 +51,12 @@ from .basic_provider_job import BasicProviderJob from .basic_provider_tools import single_gate_matrix -from .basic_provider_tools import SINGLE_QUBIT_GATES -from .basic_provider_tools import cx_gate_matrix +from .basic_provider_tools import ( + SINGLE_QUBIT_GATES, + TWO_QUBIT_GATES, + TWO_QUBIT_GATES_WITH_PARAMETERS, + THREE_QUBIT_GATES, +) from .basic_provider_tools import einsum_vecmul_index from .exceptions import BasicProviderError @@ -138,21 +142,59 @@ def _build_basic_target(self) -> Target: num_qubits=None, ) basis_gates = [ + "ccx", + "ccz", + "ch", + "cp", + "crx", + "cry", + "crz", + "cs", + "csdg", + "cswap", + "csx", + "cu", + "cu1", + "cu3", + "cx", + "cy", + "cz", + "dcx", + "delay", + "ecr", + "global_phase", "h", - "u", + "id", + "iswap", + "measure", "p", + "r", + "rccx", + "reset", + "rx", + "rxx", + "ry", + "ryy", + "rz", + "rzx", + "rzz", + "s", + "sdg", + "swap", + "sx", + "sxdg", + "t", + "tdg", + "u", "u1", "u2", "u3", - "rz", - "sx", - "x", - "cx", - "id", "unitary", - "measure", - "delay", - "reset", + "x", + "xx_minus_yy", + "xx_plus_yy", + "y", + "z", ] inst_mapping = get_standard_gate_name_mapping() for name in basis_gates: @@ -617,24 +659,41 @@ def run_experiment(self, experiment: QasmQobjExperiment) -> dict[str, ...]: value >>= 1 if value != int(operation.conditional.val, 16): continue - # Check if single gate if operation.name == "unitary": qubits = operation.qubits gate = operation.params[0] self._add_unitary(gate, qubits) + elif operation.name in ("id", "u0", "delay"): + pass + elif operation.name == "global_phase": + params = getattr(operation, "params", None) + gate = GlobalPhaseGate(*params).to_matrix() + self._add_unitary(gate, []) + # Check if single qubit gate elif operation.name in SINGLE_QUBIT_GATES: params = getattr(operation, "params", None) qubit = operation.qubits[0] gate = single_gate_matrix(operation.name, params) self._add_unitary(gate, [qubit]) - # Check if CX gate + elif operation.name in TWO_QUBIT_GATES_WITH_PARAMETERS: + params = getattr(operation, "params", None) + qubit0 = operation.qubits[0] + qubit1 = operation.qubits[1] + gate = TWO_QUBIT_GATES_WITH_PARAMETERS[operation.name](*params).to_matrix() + self._add_unitary(gate, [qubit0, qubit1]) elif operation.name in ("id", "u0"): pass - elif operation.name in ("CX", "cx"): + elif operation.name in TWO_QUBIT_GATES: qubit0 = operation.qubits[0] qubit1 = operation.qubits[1] - gate = cx_gate_matrix() + gate = TWO_QUBIT_GATES[operation.name] self._add_unitary(gate, [qubit0, qubit1]) + elif operation.name in THREE_QUBIT_GATES: + qubit0 = operation.qubits[0] + qubit1 = operation.qubits[1] + qubit2 = operation.qubits[2] + gate = THREE_QUBIT_GATES[operation.name] + self._add_unitary(gate, [qubit0, qubit1, qubit2]) # Check if reset elif operation.name == "reset": qubit = operation.qubits[0] diff --git a/releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml b/releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml new file mode 100644 index 00000000000..755403d98a3 --- /dev/null +++ b/releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml @@ -0,0 +1,5 @@ +--- +features_providers: + - | + The :class:`.BasicSimulator` python-based simulator included in :mod:`qiskit.providers.basic_provider` + now includes all the standard gates (:mod:`qiskit.circuit.library .standard_gates`) up to 3 qubits. diff --git a/test/python/providers/basic_provider/test_standard_library.py b/test/python/providers/basic_provider/test_standard_library.py new file mode 100644 index 00000000000..3d6b5c83ccc --- /dev/null +++ b/test/python/providers/basic_provider/test_standard_library.py @@ -0,0 +1,531 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +# pylint: disable=missing-function-docstring, missing-module-docstring + +import unittest + +from qiskit import QuantumCircuit +from qiskit.providers.basic_provider import BasicSimulator +import qiskit.circuit.library.standard_gates as lib +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestStandardGates(QiskitTestCase): + """Standard gates support in BasicSimulator, up to 3 qubits""" + + def setUp(self): + super().setUp() + self.seed = 43 + self.shots = 1 + self.circuit = QuantumCircuit(4) + + def test_barrier(self): + self.circuit.barrier(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_barrier_none(self): + self.circuit.barrier() + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_unitary(self): + matrix = [[0, 0, 0, 1], [0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0]] + self.circuit.unitary(matrix, [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u(self): + self.circuit.u(0.5, 1.5, 1.5, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u1(self): + self.circuit.append(lib.U1Gate(0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u2(self): + self.circuit.append(lib.U2Gate(0.5, 0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u3(self): + self.circuit.append(lib.U3Gate(0.5, 0.5, 0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ccx(self): + self.circuit.ccx(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ccz(self): + self.circuit.ccz(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ch(self): + self.circuit.ch(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cp(self): + self.circuit.cp(0, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_crx(self): + self.circuit.crx(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cry(self): + self.circuit.cry(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_crz(self): + self.circuit.crz(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cswap(self): + self.circuit.cswap(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu1(self): + self.circuit.append(lib.CU1Gate(1), [1, 2]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu3(self): + self.circuit.append(lib.CU3Gate(1, 2, 3), [1, 2]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cx(self): + self.circuit.cx(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ecr(self): + self.circuit.ecr(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cy(self): + self.circuit.cy(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cz(self): + self.circuit.cz(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_h(self): + self.circuit.h(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_id(self): + self.circuit.id(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rx(self): + self.circuit.rx(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ry(self): + self.circuit.ry(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rz(self): + self.circuit.rz(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rxx(self): + self.circuit.rxx(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rzx(self): + self.circuit.rzx(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ryy(self): + self.circuit.ryy(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rzz(self): + self.circuit.rzz(1, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_s(self): + self.circuit.s(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sdg(self): + self.circuit.sdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sx(self): + self.circuit.sx(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sxdg(self): + self.circuit.sxdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_swap(self): + self.circuit.swap(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_iswap(self): + self.circuit.iswap(1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_p(self): + self.circuit.p(1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_r(self): + self.circuit.r(0.5, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_t(self): + self.circuit.t(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_tdg(self): + self.circuit.tdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_x(self): + self.circuit.x(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_y(self): + self.circuit.y(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_z(self): + self.circuit.z(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cs(self): + self.circuit.cs(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_csdg(self): + self.circuit.csdg(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_csx(self): + self.circuit.csx(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu(self): + self.circuit.cu(0.5, 0.5, 0.5, 0.5, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_dcx(self): + self.circuit.dcx(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_delay(self): + self.circuit.delay(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_reset(self): + self.circuit.reset(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rcx(self): + self.circuit.rccx(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_global_phase(self): + qc = self.circuit + qc.append(lib.GlobalPhaseGate(0.1), []) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_xx_minus_yy(self): + self.circuit.append(lib.XXMinusYYGate(0.1, 0.2), [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_xx_plus_yy(self): + self.circuit.append(lib.XXPlusYYGate(0.1, 0.2), [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + +class TestStandardGatesTarget(QiskitTestCase): + """Standard gates, up to 3 qubits, as a target""" + + def test_target(self): + target = BasicSimulator().target + expected = { + "cz", + "u3", + "p", + "cswap", + "z", + "cu1", + "ecr", + "reset", + "ch", + "cy", + "dcx", + "crx", + "sx", + "unitary", + "csdg", + "rzz", + "measure", + "swap", + "csx", + "y", + "s", + "xx_plus_yy", + "cs", + "h", + "t", + "u", + "rxx", + "cu", + "rzx", + "ry", + "rx", + "cu3", + "tdg", + "u2", + "xx_minus_yy", + "global_phase", + "u1", + "id", + "cx", + "cp", + "rz", + "sxdg", + "x", + "ryy", + "sdg", + "ccz", + "delay", + "crz", + "iswap", + "ccx", + "cry", + "rccx", + "r", + } + self.assertEqual(set(target.operation_names), expected) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index fe209e3571a..01ec7ebf133 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -93,39 +93,77 @@ def test_str(self): pm_config.inst_map = None str_out = str(pm_config) expected = """Pass Manager Config: - initial_layout: None - basis_gates: ['h', 'u', 'p', 'u1', 'u2', 'u3', 'rz', 'sx', 'x', 'cx', 'id', 'unitary', 'measure', 'delay', 'reset'] - inst_map: None - coupling_map: None - layout_method: None - routing_method: None - translation_method: None - scheduling_method: None - instruction_durations: - backend_properties: None - approximation_degree: None - seed_transpiler: None - timing_constraints: None - unitary_synthesis_method: default - unitary_synthesis_plugin_config: None - target: Target: Basic Target - Number of qubits: None - Instructions: - h - u - p - u1 - u2 - u3 - rz - sx - x - cx - id - unitary - measure - delay - reset - +\tinitial_layout: None +\tbasis_gates: ['ccx', 'ccz', 'ch', 'cp', 'crx', 'cry', 'crz', 'cs', 'csdg', 'cswap', 'csx', 'cu', 'cu1', 'cu3', 'cx', 'cy', 'cz', 'dcx', 'delay', 'ecr', 'global_phase', 'h', 'id', 'iswap', 'measure', 'p', 'r', 'rccx', 'reset', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzx', 'rzz', 's', 'sdg', 'swap', 'sx', 'sxdg', 't', 'tdg', 'u', 'u1', 'u2', 'u3', 'unitary', 'x', 'xx_minus_yy', 'xx_plus_yy', 'y', 'z'] +\tinst_map: None +\tcoupling_map: None +\tlayout_method: None +\trouting_method: None +\ttranslation_method: None +\tscheduling_method: None +\tinstruction_durations:\u0020 +\tbackend_properties: None +\tapproximation_degree: None +\tseed_transpiler: None +\ttiming_constraints: None +\tunitary_synthesis_method: default +\tunitary_synthesis_plugin_config: None +\ttarget: Target: Basic Target +\tNumber of qubits: None +\tInstructions: +\t\tccx +\t\tccz +\t\tch +\t\tcp +\t\tcrx +\t\tcry +\t\tcrz +\t\tcs +\t\tcsdg +\t\tcswap +\t\tcsx +\t\tcu +\t\tcu1 +\t\tcu3 +\t\tcx +\t\tcy +\t\tcz +\t\tdcx +\t\tdelay +\t\tecr +\t\tglobal_phase +\t\th +\t\tid +\t\tiswap +\t\tmeasure +\t\tp +\t\tr +\t\trccx +\t\treset +\t\trx +\t\trxx +\t\try +\t\tryy +\t\trz +\t\trzx +\t\trzz +\t\ts +\t\tsdg +\t\tswap +\t\tsx +\t\tsxdg +\t\tt +\t\ttdg +\t\tu +\t\tu1 +\t\tu2 +\t\tu3 +\t\tunitary +\t\tx +\t\txx_minus_yy +\t\txx_plus_yy +\t\ty +\t\tz +\t """ self.assertEqual(str_out, expected) From 9c22197c3e4fc2794fae977469cd19d86cab021e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Apr 2024 12:40:25 +0100 Subject: [PATCH 007/159] Support standalone `expr.Var` in QPY (#11651) * Support standalone `expr.Var` in QPY This necessitates adding some extra definitions to the circuit header. When standalone variables are used, we do not re-encode the entire variable (which would take a miniumum of ~20 bytes per variable), but instead just emit an index into the order that the variables were defined at the top of the circuit. At present, each control-flow scope will store the `Var` nodes anew, so it's not a perfect only-store-once system. This is consistent with how registers are handled, though, and the QPY format isn't particularly memory optimised as things stand. * Add backwards compatibility tests --- qiskit/qpy/__init__.py | 114 +++++++- qiskit/qpy/binary_io/circuits.py | 245 +++++++++++++++--- qiskit/qpy/binary_io/value.py | 182 +++++++++++-- qiskit/qpy/common.py | 2 +- qiskit/qpy/exceptions.py | 20 ++ qiskit/qpy/formats.py | 29 +++ qiskit/qpy/type_keys.py | 20 ++ .../circuit/test_circuit_load_from_qpy.py | 170 +++++++++++- test/qpy_compat/test_qpy.py | 50 ++++ 9 files changed, 762 insertions(+), 70 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index fed09b8717a..7851db5c2a1 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -79,6 +79,12 @@ .. autoexception:: QpyError +When a lower-than-maximum target QPY version is set for serialization, but the object to be +serialized contains features that cannot be represented in that format, a subclass of +:exc:`QpyError` is raised: + +.. autoexception:: UnsupportedFeatureForVersion + Attributes: QPY_VERSION (int): The current QPY format version as of this release. This is the default value of the ``version`` keyword argument on @@ -285,12 +291,97 @@ The file header is immediately followed by the circuit payloads. Each individual circuit is composed of the following parts: -``HEADER | METADATA | REGISTERS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` +``HEADER | METADATA | REGISTERS | STANDALONE_VARS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` + +The ``STANDALONE_VARS`` are new in QPY version 12; before that, there was no data between +``REGISTERS`` and ``CUSTOM_DEFINITIONS``. There is a circuit payload for each circuit (where the total number is dictated by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_12: + +Version 12 +========== + +Version 12 adds support for: + +* circuits containing memory-owning :class:`.expr.Var` variables. + +Changes to HEADER +----------------- + +The HEADER struct for an individual circuit has added three ``uint32_t`` counts of the input, +captured and locally declared variables in the circuit. The new form looks like: + +.. code-block:: c + + struct { + uint16_t name_size; + char global_phase_type; + uint16_t global_phase_size; + uint32_t num_qubits; + uint32_t num_clbits; + uint64_t metadata_size; + uint32_t num_registers; + uint64_t num_instructions; + uint32_t num_vars; + } HEADER_V12; + +The ``HEADER_V12`` struct is followed immediately by the same name, global-phase, metadata +and register information as the V2 version of the header. Immediately following the registers is +``num_vars`` instances of ``EXPR_VAR_STANDALONE`` that define the variables in this circuit. After +that, the data continues with custom definitions and instructions as in prior versions of QPY. + + +EXPR_VAR_DECLARATION +-------------------- + +An ``EXPR_VAR_DECLARATION`` defines an :class:`.expr.Var` instance that is standalone; that is, it +represents a self-owned memory location rather than wrapping a :class:`.Clbit` or +:class:`.ClassicalRegister`. The payload is a C struct: + +.. code-block:: c + + struct { + char uuid_bytes[16]; + char usage; + uint16_t name_size; + } + +which is immediately followed by an ``EXPR_TYPE`` payload and then ``name_size`` bytes of UTF-8 +encoding string data containing the name of the variable. + +The ``char`` usage type code takes the following values: + +========= ========================================================================================= +Type code Meaning +========= ========================================================================================= +``I`` An ``input`` variable to the circuit. + +``C`` A ``capture`` variable to the circuit. + +``L`` A locally declared variable to the circuit. +========= ========================================================================================= + + +Changes to EXPR_VAR +------------------- + +The EXPR_VAR variable has gained a new type code and payload, in addition to the pre-existing ones: + +=========================== ========= ============================================================ +Python class Type code Payload +=========================== ========= ============================================================ +:class:`.UUID` ``U`` One ``uint32_t`` index of the variable into the series of + ``EXPR_VAR_STANDALONE`` variables that were written + immediately after the circuit header. +=========================== ========= ============================================================ + +Notably, this new type-code indexes into pre-defined variables from the circuit header, rather than +redefining the variable again in each location it is used. + .. _qpy_version_11: Version 11 @@ -337,17 +428,18 @@ Version 10 ========== -Version 10 adds support for symengine-native serialization for objects of type -:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks. Version -10 also adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit -0.45.0 release. +Version 10 adds support for: + +* symengine-native serialization for objects of type :class:`~.ParameterExpression` as well as + symbolic expressions in Pulse schedule blocks. +* new fields in the :class:`~.TranspileLayout` class added in the Qiskit 0.45.0 release. The symbolic_encoding field is added to the file header, and a new encoding type char is introduced, mapped to each symbolic library as follows: ``p`` refers to sympy encoding and ``e`` refers to symengine encoding. -FILE_HEADER ------------ +Changes to FILE_HEADER +---------------------- The contents of FILE_HEADER after V10 are defined as a C struct as: @@ -360,10 +452,10 @@ uint8_t qiskit_patch_version; uint64_t num_circuits; char symbolic_encoding; - } + } FILE_HEADER_V10; -LAYOUT ------- +Changes to LAYOUT +----------------- The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field. With version 10 the ``LAYOUT`` struct is now: @@ -1522,7 +1614,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. [#f3] https://docs.python.org/3/c-api/complex.html#c.Py_complex """ -from .exceptions import QpyError, QPYLoadingDeprecatedFeatureWarning +from .exceptions import QpyError, UnsupportedFeatureForVersion, QPYLoadingDeprecatedFeatureWarning from .interface import dump, load # For backward compatibility. Provide, Runtime, Experiment call these private functions. diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 40bb5850043..1cf003ff358 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -40,13 +40,39 @@ from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.qpy import common, formats, type_keys +from qiskit.qpy import common, formats, type_keys, exceptions from qiskit.qpy.binary_io import value, schedules from qiskit.quantum_info.operators import SparsePauliOp, Clifford from qiskit.synthesis import evolution as evo_synth from qiskit.transpiler.layout import Layout, TranspileLayout +def _read_header_v12(file_obj, version, vectors, metadata_deserializer=None): + data = formats.CIRCUIT_HEADER_V12._make( + struct.unpack( + formats.CIRCUIT_HEADER_V12_PACK, file_obj.read(formats.CIRCUIT_HEADER_V12_SIZE) + ) + ) + name = file_obj.read(data.name_size).decode(common.ENCODE) + global_phase = value.loads_value( + data.global_phase_type, + file_obj.read(data.global_phase_size), + version=version, + vectors=vectors, + ) + header = { + "global_phase": global_phase, + "num_qubits": data.num_qubits, + "num_clbits": data.num_clbits, + "num_registers": data.num_registers, + "num_instructions": data.num_instructions, + "num_vars": data.num_vars, + } + metadata_raw = file_obj.read(data.metadata_size) + metadata = json.loads(metadata_raw, cls=metadata_deserializer) + return header, name, metadata + + def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None): data = formats.CIRCUIT_HEADER_V2._make( struct.unpack( @@ -133,7 +159,14 @@ def _read_registers(file_obj, num_registers): def _loads_instruction_parameter( - type_key, data_bytes, version, vectors, registers, circuit, use_symengine + type_key, + data_bytes, + version, + vectors, + registers, + circuit, + use_symengine, + standalone_vars, ): if type_key == type_keys.Program.CIRCUIT: param = common.data_from_binary(data_bytes, read_circuit, version=version) @@ -152,6 +185,7 @@ def _loads_instruction_parameter( registers=registers, circuit=circuit, use_symengine=use_symengine, + standalone_vars=standalone_vars, ) ) elif type_key == type_keys.Value.INTEGER: @@ -172,6 +206,7 @@ def _loads_instruction_parameter( clbits=clbits, cregs=registers["c"], use_symengine=use_symengine, + standalone_vars=standalone_vars, ) return param @@ -186,7 +221,14 @@ def _loads_register_param(data_bytes, circuit, registers): def _read_instruction( - file_obj, circuit, registers, custom_operations, version, vectors, use_symengine + file_obj, + circuit, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ): if version < 5: instruction = formats.CIRCUIT_INSTRUCTION._make( @@ -224,6 +266,7 @@ def _read_instruction( clbits=circuit.clbits, cregs=registers["c"], use_symengine=use_symengine, + standalone_vars=standalone_vars, ) # Load Arguments if circuit is not None: @@ -252,14 +295,28 @@ def _read_instruction( for _param in range(instruction.num_parameters): type_key, data_bytes = common.read_generic_typed_data(file_obj) param = _loads_instruction_parameter( - type_key, data_bytes, version, vectors, registers, circuit, use_symengine + type_key, + data_bytes, + version, + vectors, + registers, + circuit, + use_symengine, + standalone_vars, ) params.append(param) # Load Gate object if gate_name in {"Gate", "Instruction", "ControlledGate"}: inst_obj = _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ) inst_obj.condition = condition if instruction.label_size > 0: @@ -270,7 +327,14 @@ def _read_instruction( return None elif gate_name in custom_operations: inst_obj = _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ) inst_obj.condition = condition if instruction.label_size > 0: @@ -361,7 +425,14 @@ def _read_instruction( def _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ): if version >= 5: ( @@ -394,7 +465,14 @@ def _parse_custom_operation( if version >= 5 and type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: with io.BytesIO(base_gate_raw) as base_gate_obj: base_gate = _read_instruction( - base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine + base_gate_obj, + None, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ) if ctrl_state < 2**num_ctrl_qubits - 1: # If open controls, we need to discard the control suffix when setting the name. @@ -413,7 +491,14 @@ def _parse_custom_operation( if version >= 11 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION: with io.BytesIO(base_gate_raw) as base_gate_obj: base_gate = _read_instruction( - base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine + base_gate_obj, + None, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ) inst_obj = AnnotatedOperation(base_op=base_gate, modifiers=params) return inst_obj @@ -572,10 +657,12 @@ def _dumps_register(register, index_map): return b"\x00" + str(index_map["c"][register]).encode(common.ENCODE) -def _dumps_instruction_parameter(param, index_map, use_symengine): +def _dumps_instruction_parameter( + param, index_map, use_symengine, *, version, standalone_var_indices +): if isinstance(param, QuantumCircuit): type_key = type_keys.Program.CIRCUIT - data_bytes = common.data_to_binary(param, write_circuit) + data_bytes = common.data_to_binary(param, write_circuit, version=version) elif isinstance(param, Modifier): type_key = type_keys.Value.MODIFIER data_bytes = common.data_to_binary(param, _write_modifier) @@ -585,7 +672,12 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): elif isinstance(param, tuple): type_key = type_keys.Container.TUPLE data_bytes = common.sequence_to_binary( - param, _dumps_instruction_parameter, index_map=index_map, use_symengine=use_symengine + param, + _dumps_instruction_parameter, + index_map=index_map, + use_symengine=use_symengine, + version=version, + standalone_var_indices=standalone_var_indices, ) elif isinstance(param, int): # TODO This uses little endian. This should be fixed in next QPY version. @@ -600,14 +692,25 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): data_bytes = _dumps_register(param, index_map) else: type_key, data_bytes = value.dumps_value( - param, index_map=index_map, use_symengine=use_symengine + param, + index_map=index_map, + use_symengine=use_symengine, + standalone_var_indices=standalone_var_indices, ) return type_key, data_bytes # pylint: disable=too-many-boolean-expressions -def _write_instruction(file_obj, instruction, custom_operations, index_map, use_symengine, version): +def _write_instruction( + file_obj, + instruction, + custom_operations, + index_map, + use_symengine, + version, + standalone_var_indices=None, +): if isinstance(instruction.operation, Instruction): gate_class_name = instruction.operation.base_class.__name__ else: @@ -702,7 +805,12 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_ file_obj.write(gate_class_name) file_obj.write(label_raw) if condition_type is type_keys.Condition.EXPRESSION: - value.write_value(file_obj, op_condition, index_map=index_map) + value.write_value( + file_obj, + op_condition, + index_map=index_map, + standalone_var_indices=standalone_var_indices, + ) else: file_obj.write(condition_register) # Encode instruction args @@ -718,7 +826,13 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_ file_obj.write(instruction_arg_raw) # Encode instruction params for param in instruction_params: - type_key, data_bytes = _dumps_instruction_parameter(param, index_map, use_symengine) + type_key, data_bytes = _dumps_instruction_parameter( + param, + index_map, + use_symengine, + version=version, + standalone_var_indices=standalone_var_indices, + ) common.write_generic_typed_data(file_obj, type_key, data_bytes) return custom_operations_list @@ -788,7 +902,9 @@ def _write_modifier(file_obj, modifier): file_obj.write(modifier_data) -def _write_custom_operation(file_obj, name, operation, custom_operations, use_symengine, version): +def _write_custom_operation( + file_obj, name, operation, custom_operations, use_symengine, version, *, standalone_var_indices +): type_key = type_keys.CircuitInstruction.assign(operation) has_definition = False size = 0 @@ -813,7 +929,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy # Build internal definition to support overloaded subclasses by # calling definition getter on object operation.definition # pylint: disable=pointless-statement - data = common.data_to_binary(operation._definition, write_circuit) + data = common.data_to_binary(operation._definition, write_circuit, version=version) size = len(data) num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state @@ -823,7 +939,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy base_gate = operation.base_op elif operation.definition is not None: has_definition = True - data = common.data_to_binary(operation.definition, write_circuit) + data = common.data_to_binary(operation.definition, write_circuit, version=version) size = len(data) if base_gate is None: base_gate_raw = b"" @@ -836,6 +952,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy {}, use_symengine, version, + standalone_var_indices=standalone_var_indices, ) base_gate_raw = base_gate_buffer.getvalue() name_raw = name.encode(common.ENCODE) @@ -1103,23 +1220,49 @@ def write_circuit( num_registers = num_qregs + num_cregs # Write circuit header - header_raw = formats.CIRCUIT_HEADER_V2( - name_size=len(circuit_name), - global_phase_type=global_phase_type, - global_phase_size=len(global_phase_data), - num_qubits=circuit.num_qubits, - num_clbits=circuit.num_clbits, - metadata_size=metadata_size, - num_registers=num_registers, - num_instructions=num_instructions, - ) - header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) - file_obj.write(header) - file_obj.write(circuit_name) - file_obj.write(global_phase_data) - file_obj.write(metadata_raw) - # Write header payload - file_obj.write(registers_raw) + if version >= 12: + header_raw = formats.CIRCUIT_HEADER_V12( + name_size=len(circuit_name), + global_phase_type=global_phase_type, + global_phase_size=len(global_phase_data), + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + metadata_size=metadata_size, + num_registers=num_registers, + num_instructions=num_instructions, + num_vars=circuit.num_vars, + ) + header = struct.pack(formats.CIRCUIT_HEADER_V12_PACK, *header_raw) + file_obj.write(header) + file_obj.write(circuit_name) + file_obj.write(global_phase_data) + file_obj.write(metadata_raw) + # Write header payload + file_obj.write(registers_raw) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + else: + if circuit.num_vars: + raise exceptions.UnsupportedFeatureForVersion( + "circuits containing realtime variables", required=12, target=version + ) + header_raw = formats.CIRCUIT_HEADER_V2( + name_size=len(circuit_name), + global_phase_type=global_phase_type, + global_phase_size=len(global_phase_data), + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + metadata_size=metadata_size, + num_registers=num_registers, + num_instructions=num_instructions, + ) + header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) + file_obj.write(header) + file_obj.write(circuit_name) + file_obj.write(global_phase_data) + file_obj.write(metadata_raw) + file_obj.write(registers_raw) + standalone_var_indices = {} + instruction_buffer = io.BytesIO() custom_operations = {} index_map = {} @@ -1127,7 +1270,13 @@ def write_circuit( index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)} for instruction in circuit.data: _write_instruction( - instruction_buffer, instruction, custom_operations, index_map, use_symengine, version + instruction_buffer, + instruction, + custom_operations, + index_map, + use_symengine, + version, + standalone_var_indices=standalone_var_indices, ) with io.BytesIO() as custom_operations_buffer: @@ -1145,6 +1294,7 @@ def write_circuit( custom_operations, use_symengine, version, + standalone_var_indices=standalone_var_indices, ) ) @@ -1186,16 +1336,21 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa vectors = {} if version < 2: header, name, metadata = _read_header(file_obj, metadata_deserializer=metadata_deserializer) - else: + elif version < 12: header, name, metadata = _read_header_v2( file_obj, version, vectors, metadata_deserializer=metadata_deserializer ) + else: + header, name, metadata = _read_header_v12( + file_obj, version, vectors, metadata_deserializer=metadata_deserializer + ) global_phase = header["global_phase"] num_qubits = header["num_qubits"] num_clbits = header["num_clbits"] num_registers = header["num_registers"] num_instructions = header["num_instructions"] + num_vars = header.get("num_vars", 0) # `out_registers` is two "name: register" maps segregated by type for the rest of QPY, and # `all_registers` is the complete ordered list used to construct the `QuantumCircuit`. out_registers = {"q": {}, "c": {}} @@ -1252,6 +1407,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) circ = QuantumCircuit( out_bits["q"], out_bits["c"], @@ -1259,11 +1415,22 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa name=name, global_phase=global_phase, metadata=metadata, + inputs=var_segments[type_keys.ExprVarDeclaration.INPUT], + captures=var_segments[type_keys.ExprVarDeclaration.CAPTURE], ) + for declaration in var_segments[type_keys.ExprVarDeclaration.LOCAL]: + circ.add_uninitialized_var(declaration) custom_operations = _read_custom_operations(file_obj, version, vectors) for _instruction in range(num_instructions): _read_instruction( - file_obj, circ, out_registers, custom_operations, version, vectors, use_symengine + file_obj, + circ, + out_registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_var_indices, ) # Read calibrations diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 1c11d4ad27c..a3d7ff08813 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -95,11 +95,12 @@ def _write_parameter_expression(file_obj, obj, use_symengine): class _ExprWriter(expr.ExprVisitor[None]): - __slots__ = ("file_obj", "clbit_indices") + __slots__ = ("file_obj", "clbit_indices", "standalone_var_indices") - def __init__(self, file_obj, clbit_indices): + def __init__(self, file_obj, clbit_indices, standalone_var_indices): self.file_obj = file_obj self.clbit_indices = clbit_indices + self.standalone_var_indices = standalone_var_indices def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") @@ -107,7 +108,15 @@ def visit_generic(self, node, /): def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) _write_expr_type(self.file_obj, node.type) - if isinstance(node.var, Clbit): + if node.standalone: + self.file_obj.write(type_keys.ExprVar.UUID) + self.file_obj.write( + struct.pack( + formats.EXPR_VAR_UUID_PACK, + *formats.EXPR_VAR_UUID(self.standalone_var_indices[node]), + ) + ) + elif isinstance(node.var, Clbit): self.file_obj.write(type_keys.ExprVar.CLBIT) self.file_obj.write( struct.pack( @@ -178,8 +187,13 @@ def visit_binary(self, node, /): node.right.accept(self) -def _write_expr(file_obj, node: expr.Expr, clbit_indices: collections.abc.Mapping[Clbit, int]): - node.accept(_ExprWriter(file_obj, clbit_indices)) +def _write_expr( + file_obj, + node: expr.Expr, + clbit_indices: collections.abc.Mapping[Clbit, int], + standalone_var_indices: collections.abc.Mapping[expr.Var, int], +): + node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices)) def _write_expr_type(file_obj, type_: types.Type): @@ -315,12 +329,18 @@ def _read_expr( file_obj, clbits: collections.abc.Sequence[Clbit], cregs: collections.abc.Mapping[str, ClassicalRegister], + standalone_vars: collections.abc.Sequence[expr.Var], ) -> expr.Expr: # pylint: disable=too-many-return-statements type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) type_ = _read_expr_type(file_obj) if type_key == type_keys.Expression.VAR: var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) + if var_type_key == type_keys.ExprVar.UUID: + payload = formats.EXPR_VAR_UUID._make( + struct.unpack(formats.EXPR_VAR_UUID_PACK, file_obj.read(formats.EXPR_VAR_UUID_SIZE)) + ) + return standalone_vars[payload.var_index] if var_type_key == type_keys.ExprVar.CLBIT: payload = formats.EXPR_VAR_CLBIT._make( struct.unpack( @@ -360,14 +380,20 @@ def _read_expr( payload = formats.EXPRESSION_CAST._make( struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) ) - return expr.Cast(_read_expr(file_obj, clbits, cregs), type_, implicit=payload.implicit) + return expr.Cast( + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, implicit=payload.implicit + ) if type_key == type_keys.Expression.UNARY: payload = formats.EXPRESSION_UNARY._make( struct.unpack( formats.EXPRESSION_UNARY_PACK, file_obj.read(formats.EXPRESSION_UNARY_SIZE) ) ) - return expr.Unary(expr.Unary.Op(payload.opcode), _read_expr(file_obj, clbits, cregs), type_) + return expr.Unary( + expr.Unary.Op(payload.opcode), + _read_expr(file_obj, clbits, cregs, standalone_vars), + type_, + ) if type_key == type_keys.Expression.BINARY: payload = formats.EXPRESSION_BINARY._make( struct.unpack( @@ -376,8 +402,8 @@ def _read_expr( ) return expr.Binary( expr.Binary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs), - _read_expr(file_obj, clbits, cregs), + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) raise exceptions.QpyError("Invalid classical-expression Expr key '{type_key}'") @@ -395,7 +421,80 @@ def _read_expr_type(file_obj) -> types.Type: raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def dumps_value(obj, *, index_map=None, use_symengine=False): +def read_standalone_vars(file_obj, num_vars): + """Read the ``num_vars`` standalone variable declarations from the file. + + Args: + file_obj (File): a file-like object to read from. + num_vars (int): the number of variables to read. + + Returns: + tuple[dict, list]: the first item is a mapping of the ``ExprVarDeclaration`` type keys to + the variables defined by that type key, and the second is the total order of variable + declarations. + """ + read_vars = { + type_keys.ExprVarDeclaration.INPUT: [], + type_keys.ExprVarDeclaration.CAPTURE: [], + type_keys.ExprVarDeclaration.LOCAL: [], + } + var_order = [] + for _ in range(num_vars): + data = formats.EXPR_VAR_DECLARATION._make( + struct.unpack( + formats.EXPR_VAR_DECLARATION_PACK, + file_obj.read(formats.EXPR_VAR_DECLARATION_SIZE), + ) + ) + type_ = _read_expr_type(file_obj) + name = file_obj.read(data.name_size).decode(common.ENCODE) + var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) + read_vars[data.usage].append(var) + var_order.append(var) + return read_vars, var_order + + +def _write_standalone_var(file_obj, var, type_key): + name = var.name.encode(common.ENCODE) + file_obj.write( + struct.pack( + formats.EXPR_VAR_DECLARATION_PACK, + *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), + ) + ) + _write_expr_type(file_obj, var.type) + file_obj.write(name) + + +def write_standalone_vars(file_obj, circuit): + """Write the standalone variables out from a circuit. + + Args: + file_obj (File): the file-like object to write to. + circuit (QuantumCircuit): the circuit to take the variables from. + + Returns: + dict[expr.Var, int]: a mapping of the variables written to the index that they were written + at. + """ + index = 0 + out = {} + for var in circuit.iter_input_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + out[var] = index + index += 1 + for var in circuit.iter_captured_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + out[var] = index + index += 1 + for var in circuit.iter_declared_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + out[var] = index + index += 1 + return out + + +def dumps_value(obj, *, index_map=None, use_symengine=False, standalone_var_indices=None): """Serialize input value object. Args: @@ -407,6 +506,8 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_var_indices (dict): Dictionary that maps standalone :class:`.expr.Var` entries to + the index that should be used to refer to them. Returns: tuple: TypeKey and binary data. @@ -438,14 +539,20 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): ) elif type_key == type_keys.Value.EXPRESSION: clbit_indices = {} if index_map is None else index_map["c"] - binary_data = common.data_to_binary(obj, _write_expr, clbit_indices=clbit_indices) + standalone_var_indices = {} if standalone_var_indices is None else standalone_var_indices + binary_data = common.data_to_binary( + obj, + _write_expr, + clbit_indices=clbit_indices, + standalone_var_indices=standalone_var_indices, + ) else: raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") return type_key, binary_data -def write_value(file_obj, obj, *, index_map=None, use_symengine=False): +def write_value(file_obj, obj, *, index_map=None, use_symengine=False, standalone_var_indices=None): """Write a value to the file like object. Args: @@ -458,13 +565,28 @@ def write_value(file_obj, obj, *, index_map=None, use_symengine=False): native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_var_indices (dict): Dictionary that maps standalone :class:`.expr.Var` entries to + the index that should be used to refer to them. """ - type_key, data = dumps_value(obj, index_map=index_map, use_symengine=use_symengine) + type_key, data = dumps_value( + obj, + index_map=index_map, + use_symengine=use_symengine, + standalone_var_indices=standalone_var_indices, + ) common.write_generic_typed_data(file_obj, type_key, data) def loads_value( - type_key, binary_data, version, vectors, *, clbits=(), cregs=None, use_symengine=False + type_key, + binary_data, + version, + vectors, + *, + clbits=(), + cregs=None, + use_symengine=False, + standalone_vars=(), ): """Deserialize input binary data to value object. @@ -479,6 +601,8 @@ def loads_value( native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_vars (Sequence[Var]): standalone :class:`.expr.Var` nodes in the order that they + were declared by the circuit header. Returns: any: Deserialized value object. @@ -520,12 +644,27 @@ def loads_value( use_symengine=use_symengine, ) if type_key == type_keys.Value.EXPRESSION: - return common.data_from_binary(binary_data, _read_expr, clbits=clbits, cregs=cregs or {}) + return common.data_from_binary( + binary_data, + _read_expr, + clbits=clbits, + cregs=cregs or {}, + standalone_vars=standalone_vars, + ) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") -def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengine=False): +def read_value( + file_obj, + version, + vectors, + *, + clbits=(), + cregs=None, + use_symengine=False, + standalone_vars=(), +): """Read a value from the file like object. Args: @@ -538,6 +677,8 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengi native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_vars (Sequence[expr.Var]): standalone variables in the order they were defined in + the QPY payload. Returns: any: Deserialized value object. @@ -545,5 +686,12 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengi type_key, data = common.read_generic_typed_data(file_obj) return loads_value( - type_key, data, version, vectors, clbits=clbits, cregs=cregs, use_symengine=use_symengine + type_key, + data, + version, + vectors, + clbits=clbits, + cregs=cregs, + use_symengine=use_symengine, + standalone_vars=standalone_vars, ) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 7cc11fb7ca0..048320d5cad 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 11 +QPY_VERSION = 12 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/exceptions.py b/qiskit/qpy/exceptions.py index c6cdb4303a6..5662e602937 100644 --- a/qiskit/qpy/exceptions.py +++ b/qiskit/qpy/exceptions.py @@ -28,6 +28,26 @@ def __str__(self): return repr(self.message) +class UnsupportedFeatureForVersion(QpyError): + """QPY error raised when the target dump version is too low for a feature that is present in the + object to be serialized.""" + + def __init__(self, feature: str, required: int, target: int): + """ + Args: + feature: a description of the problematic feature. + required: the minimum version of QPY that would be required to represent this + feature. + target: the version of QPY that is being used in the serialization. + """ + self.feature = feature + self.required = required + self.target = target + super().__init__( + f"Dumping QPY version {target}, but version {required} is required for: {feature}." + ) + + class QPYLoadingDeprecatedFeatureWarning(QiskitWarning): """Visible deprecation warning for QPY loading functions without a stable point in the call stack.""" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 958bebd8dad..a48a9ea777f 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -42,6 +42,24 @@ FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) + +CIRCUIT_HEADER_V12 = namedtuple( + "HEADER", + [ + "name_size", + "global_phase_type", + "global_phase_size", + "num_qubits", + "num_clbits", + "metadata_size", + "num_registers", + "num_instructions", + "num_vars", + ], +) +CIRCUIT_HEADER_V12_PACK = "!H1cHIIQIQI" +CIRCUIT_HEADER_V12_SIZE = struct.calcsize(CIRCUIT_HEADER_V12_PACK) + # CIRCUIT_HEADER_V2 CIRCUIT_HEADER_V2 = namedtuple( "HEADER", @@ -309,6 +327,13 @@ INITIAL_LAYOUT_BIT_PACK = "!ii" INITIAL_LAYOUT_BIT_SIZE = struct.calcsize(INITIAL_LAYOUT_BIT_PACK) +# EXPR_VAR_DECLARATION + +EXPR_VAR_DECLARATION = namedtuple("EXPR_VAR_DECLARATION", ["uuid_bytes", "usage", "name_size"]) +EXPR_VAR_DECLARATION_PACK = "!16scH" +EXPR_VAR_DECLARATION_SIZE = struct.calcsize(EXPR_VAR_DECLARATION_PACK) + + # EXPRESSION EXPRESSION_DISCRIMINATOR_SIZE = 1 @@ -351,6 +376,10 @@ EXPR_VAR_REGISTER_PACK = "!H" EXPR_VAR_REGISTER_SIZE = struct.calcsize(EXPR_VAR_REGISTER_PACK) +EXPR_VAR_UUID = namedtuple("EXPR_VAR_UUID", ["var_index"]) +EXPR_VAR_UUID_PACK = "!H" +EXPR_VAR_UUID_SIZE = struct.calcsize(EXPR_VAR_UUID_PACK) + # EXPR_VALUE diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index dd0e7fe2269..6ec85115b55 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -16,6 +16,7 @@ QPY Type keys for several namespace. """ +import uuid from abc import abstractmethod from enum import Enum, IntEnum @@ -471,6 +472,22 @@ def retrieve(cls, type_key): raise NotImplementedError +class ExprVarDeclaration(TypeKeyBase): + """Type keys for the ``EXPR_VAR_DECLARATION`` QPY item.""" + + INPUT = b"I" + CAPTURE = b"C" + LOCAL = b"L" + + @classmethod + def assign(cls, obj): + raise NotImplementedError + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + class ExprType(TypeKeyBase): """Type keys for the ``EXPR_TYPE`` QPY item.""" @@ -496,9 +513,12 @@ class ExprVar(TypeKeyBase): CLBIT = b"C" REGISTER = b"R" + UUID = b"U" @classmethod def assign(cls, obj): + if isinstance(obj, uuid.UUID): + return cls.UUID if isinstance(obj, Clbit): return cls.CLBIT if isinstance(obj, ClassicalRegister): diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 766d555bda5..efae9697f18 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -22,7 +22,7 @@ import numpy as np from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, pulse -from qiskit.circuit import CASE_DEFAULT +from qiskit.circuit import CASE_DEFAULT, IfElseOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.classical import expr, types from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.quantumregister import Qubit @@ -57,7 +57,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parametervector import ParameterVector from qiskit.synthesis import LieTrotter, SuzukiTrotter -from qiskit.qpy import dump, load +from qiskit.qpy import dump, load, UnsupportedFeatureForVersion, QPY_COMPATIBILITY_VERSION from qiskit.quantum_info import Pauli, SparsePauliOp, Clifford from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate @@ -84,6 +84,26 @@ def assertDeprecatedBitProperties(self, original, roundtripped): original_clbits, roundtripped_clbits = zip(*owned_clbits) self.assertEqual(original_clbits, roundtripped_clbits) + def assertMinimalVarEqual(self, left, right): + """Replacement for asserting `QuantumCircuit` equality for use in `Var` tests, for use while + the `DAGCircuit` does not yet allow full equality checks. This should be removed and the + tests changed to directly call `assertEqual` once possible. + + This filters out instructions that have `QuantumCircuit` parameters in the data comparison + (such as control-flow ops), which need to be handled separately.""" + self.assertEqual(list(left.iter_input_vars()), list(right.iter_input_vars())) + self.assertEqual(list(left.iter_declared_vars()), list(right.iter_declared_vars())) + self.assertEqual(list(left.iter_captured_vars()), list(right.iter_captured_vars())) + + def filter_ops(data): + return [ + ins + for ins in data + if not any(isinstance(x, QuantumCircuit) for x in ins.operation.params) + ] + + self.assertEqual(filter_ops(left.data), filter_ops(right.data)) + def test_qpy_full_path(self): """Test full path qpy serialization for basic circuit.""" qr_a = QuantumRegister(4, "a") @@ -1760,6 +1780,152 @@ def test_annotated_operations_iterative(self): new_circuit = load(fptr)[0] self.assertEqual(circuit, new_circuit) + def test_load_empty_vars(self): + """Test loading empty circuits with variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + all_vars = { + a: expr.lift(False), + b: expr.lift(3, type=b.type), + expr.Var.new("θψφ", types.Bool()): expr.logic_not(a), + expr.Var.new("🐍🐍🐍", types.Uint(8)): expr.bit_and(b, b), + } + + inputs = QuantumCircuit(inputs=list(all_vars)) + with io.BytesIO() as fptr: + dump(inputs, fptr) + fptr.seek(0) + new_inputs = load(fptr)[0] + self.assertMinimalVarEqual(inputs, new_inputs) + self.assertDeprecatedBitProperties(inputs, new_inputs) + + # Reversed order just to check there's no sorting shenanigans. + captures = QuantumCircuit(captures=list(all_vars)[::-1]) + with io.BytesIO() as fptr: + dump(captures, fptr) + fptr.seek(0) + new_captures = load(fptr)[0] + self.assertMinimalVarEqual(captures, new_captures) + self.assertDeprecatedBitProperties(captures, new_captures) + + declares = QuantumCircuit(declarations=all_vars) + with io.BytesIO() as fptr: + dump(declares, fptr) + fptr.seek(0) + new_declares = load(fptr)[0] + self.assertMinimalVarEqual(declares, new_declares) + self.assertDeprecatedBitProperties(declares, new_declares) + + def test_load_empty_vars_if(self): + """Test loading circuit with vars in if/else closures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("θψφ", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + d = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a]) + qc.add_var(b, expr.logic_not(a)) + qc.add_var(c, expr.lift(0, c.type)) + with qc.if_test(b) as else_: + qc.store(c, expr.lift(3, c.type)) + with else_: + qc.add_var(d, expr.lift(7, d.type)) + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_if_else = qc.data[-1].operation + new_if_else = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_if_else, IfElseOp) + self.assertIsInstance(new_if_else, IfElseOp) + self.assertEqual(len(old_if_else.blocks), len(new_if_else.blocks)) + + for old, new in zip(old_if_else.blocks, new_if_else.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + def test_load_empty_vars_while(self): + """Test loading circuit with vars in while closures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("θψφ", types.Bool()) + c = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a]) + qc.add_var(b, expr.logic_not(a)) + with qc.while_loop(b): + qc.add_var(c, expr.lift(7, c.type)) + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_while = qc.data[-1].operation + new_while = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_while, WhileLoopOp) + self.assertIsInstance(new_while, WhileLoopOp) + self.assertEqual(len(old_while.blocks), len(new_while.blocks)) + + for old, new in zip(old_while.blocks, new_while.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + def test_load_empty_vars_switch(self): + """Test loading circuit with vars in switch closures.""" + a = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(1, 1, inputs=[a]) + qc.measure(0, 0) + b_outer = qc.add_var("b", False) + with qc.switch(a) as case: + with case(0): + qc.store(b_outer, True) + with case(1): + qc.store(qc.clbits[0], False) + with case(2): + # Explicit shadowing. + qc.add_var("b", True) + with case(3): + qc.store(a, expr.lift(1, a.type)) + with case(case.DEFAULT): + pass + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_switch = qc.data[-1].operation + new_switch = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_switch, SwitchCaseOp) + self.assertIsInstance(new_switch, SwitchCaseOp) + self.assertEqual(len(old_switch.blocks), len(new_switch.blocks)) + + for old, new in zip(old_switch.blocks, new_switch.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 12)) + def test_pre_v12_rejects_standalone_var(self, version): + """Test that dumping to older QPY versions rejects standalone vars.""" + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(inputs=[a]) + with io.BytesIO() as fptr, self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 12 is required.*realtime variables" + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 345d9dc0a44..58ee1abc2a2 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -754,6 +754,54 @@ def generate_control_flow_expr(): return [qc1, qc2, qc3, qc4] +def generate_standalone_var(): + """Circuits that use standalone variables.""" + import uuid + from qiskit.circuit.classical import expr, types + + # This is the low-level, non-preferred way to construct variables, but we need the UUIDs to be + # deterministic between separate invocations of the script. + uuids = [ + uuid.UUID(bytes=b"hello, qpy world", version=4), + uuid.UUID(bytes=b"not a good uuid4", version=4), + uuid.UUID(bytes=b"but it's ok here", version=4), + uuid.UUID(bytes=b"any old 16 bytes", version=4), + uuid.UUID(bytes=b"and another load", version=4), + ] + a = expr.Var(uuids[0], types.Bool(), name="a") + b = expr.Var(uuids[1], types.Bool(), name="θψφ") + b_other = expr.Var(uuids[2], types.Bool(), name=b.name) + c = expr.Var(uuids[3], types.Uint(8), name="🐍🐍🐍") + d = expr.Var(uuids[4], types.Uint(8), name="d") + + qc = QuantumCircuit(1, 1, inputs=[a], name="standalone_var") + qc.add_var(b, expr.logic_not(a)) + + qc.add_var(c, expr.lift(0, c.type)) + with qc.if_test(b) as else_: + qc.store(c, expr.lift(3, c.type)) + with qc.while_loop(b): + qc.add_var(c, expr.lift(7, c.type)) + with else_: + qc.add_var(d, expr.lift(7, d.type)) + + qc.measure(0, 0) + with qc.switch(c) as case: + with case(0): + qc.store(b, True) + with case(1): + qc.store(qc.clbits[0], False) + with case(2): + # Explicit shadowing. + qc.add_var(b_other, True) + with case(3): + qc.store(a, False) + with case(case.DEFAULT): + pass + + return [qc] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -802,6 +850,8 @@ def generate_circuits(version_parts): output_circuits["clifford.qpy"] = generate_clifford_circuits() if version_parts >= (1, 0, 0): output_circuits["annotated.qpy"] = generate_annotated_circuits() + if version_parts >= (1, 1, 0): + output_circuits["standalone_vars.qpy"] = generate_standalone_var() return output_circuits From 958cc9b565f52e7326165d3b631d455e3c601972 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Apr 2024 10:42:24 -0400 Subject: [PATCH 008/159] Avoid intermediate DAGCircuit construction in 2q synthesis (#12179) This commit builds on #12109 which added a dag output to the two qubit decomposers that are then used by unitary synthesis to add a mode of operation in unitary synthesis that avoids intermediate dag creation. To do this efficiently this requires changing the UnitarySynthesis pass to rebuild the DAG instead of doing a node substitution. --- .../passes/synthesis/unitary_synthesis.py | 131 +++++++++++++----- 1 file changed, 98 insertions(+), 33 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 5d919661a83..a30411d16a9 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -39,6 +39,7 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, + GATE_NAME_MAP, ) from qiskit.quantum_info import Operator from qiskit.circuit import ControlFlowOp, Gate, Parameter @@ -293,7 +294,7 @@ def __init__( natural_direction: bool | None = None, synth_gates: list[str] | None = None, method: str = "default", - min_qubits: int = None, + min_qubits: int = 0, plugin_config: dict = None, target: Target = None, ): @@ -499,27 +500,55 @@ def _run_main_loop( ] ) - for node in dag.named_nodes(*self._synth_gates): - if self._min_qubits is not None and len(node.qargs) < self._min_qubits: - continue - synth_dag = None - unitary = node.op.to_matrix() - n_qubits = len(node.qargs) - if (plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits) or ( - plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits - ): - method, kwargs = default_method, default_kwargs + out_dag = dag.copy_empty_like() + for node in dag.topological_op_nodes(): + if node.op.name == "unitary" and len(node.qargs) >= self._min_qubits: + synth_dag = None + unitary = node.op.to_matrix() + n_qubits = len(node.qargs) + if ( + plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits + ) or (plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits): + method, kwargs = default_method, default_kwargs + else: + method, kwargs = plugin_method, plugin_kwargs + if method.supports_coupling_map: + kwargs["coupling_map"] = ( + self._coupling_map, + [qubit_indices[x] for x in node.qargs], + ) + synth_dag = method.run(unitary, **kwargs) + if synth_dag is None: + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + continue + if isinstance(synth_dag, DAGCircuit): + qubit_map = dict(zip(synth_dag.qubits, node.qargs)) + for node in synth_dag.topological_op_nodes(): + out_dag.apply_operation_back( + node.op, (qubit_map[x] for x in node.qargs), check=False + ) + out_dag.global_phase += synth_dag.global_phase + else: + node_list, global_phase, gate = synth_dag + qubits = node.qargs + for ( + op_name, + params, + qargs, + ) in node_list: + if op_name == "USER_GATE": + op = gate + else: + op = GATE_NAME_MAP[op_name](*params) + out_dag.apply_operation_back( + op, + (qubits[x] for x in qargs), + check=False, + ) + out_dag.global_phase += global_phase else: - method, kwargs = plugin_method, plugin_kwargs - if method.supports_coupling_map: - kwargs["coupling_map"] = ( - self._coupling_map, - [qubit_indices[x] for x in node.qargs], - ) - synth_dag = method.run(unitary, **kwargs) - if synth_dag is not None: - dag.substitute_node_with_dag(node, synth_dag) - return dag + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + return out_dag def _build_gate_lengths(props=None, target=None): @@ -893,6 +922,20 @@ def run(self, unitary, **options): decomposers2q = [decomposer2q] if decomposer2q is not None else [] # choose the cheapest output among synthesized circuits synth_circuits = [] + # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to + # store and can instead manually create the synthesized gates directly in the output dag + if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): + preferred_direction = _preferred_direction( + decomposers2q[0], + qubits, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + ) + return self._synth_su4_no_dag( + unitary, decomposers2q[0], preferred_direction, approximation_degree + ) for decomposer2q in decomposers2q: preferred_direction = _preferred_direction( decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors @@ -919,6 +962,24 @@ def run(self, unitary, **options): return synth_circuit return circuit_to_dag(synth_circuit) + def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) + if not preferred_direction: + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for op_name, _params, qubits in synth_circ: + if op_name in {"USER_GATE", "cx"}: + synth_direction = qubits + if synth_direction is not None and synth_direction != preferred_direction: + # TODO: Avoid using a dag to correct the synthesis direction + return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): approximate = not approximation_degree == 1.0 synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) @@ -932,16 +993,20 @@ def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_d if inst.op.num_qubits == 2: synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] if synth_direction is not None and synth_direction != preferred_direction: - su4_mat_mm = su4_mat.copy() - su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] - su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] - synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) - out_dag = DAGCircuit() - out_dag.global_phase = synth_circ.global_phase - out_dag.add_qubits(list(reversed(synth_circ.qubits))) - flip_bits = out_dag.qubits[::-1] - for node in synth_circ.topological_op_nodes(): - qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - out_dag.apply_operation_back(node.op, qubits, check=False) - return out_dag + return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) return synth_circ + + def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): + approximate = not approximation_degree == 1.0 + su4_mat_mm = su4_mat.copy() + su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] + su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] + synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) + out_dag = DAGCircuit() + out_dag.global_phase = synth_circ.global_phase + out_dag.add_qubits(list(reversed(synth_circ.qubits))) + flip_bits = out_dag.qubits[::-1] + for node in synth_circ.topological_op_nodes(): + qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) + out_dag.apply_operation_back(node.op, qubits, check=False) + return out_dag From 95476b747163479fca5ad2ec81b4b3e9fd7fd657 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:11:04 +0400 Subject: [PATCH 009/159] fix a type hint and add tests for iterable (#12309) --- qiskit/primitives/backend_sampler_v2.py | 2 +- .../primitives/test_backend_estimator_v2.py | 12 ++++++++++++ .../primitives/test_backend_sampler_v2.py | 17 +++++++++++++++++ .../primitives/test_statevector_estimator.py | 9 +++++++++ .../primitives/test_statevector_sampler.py | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index 51d1ded1500..87507e1d54d 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -142,7 +142,7 @@ def _validate_pubs(self, pubs: list[SamplerPub]): UserWarning, ) - def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: + def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[PubResult]: pub_dict = defaultdict(list) # consolidate pubs with the same number of shots for i, pub in enumerate(pubs): diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 2af6b15b877..6728d57e3fd 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -461,6 +461,18 @@ def test_job_size_limit_backend_v1(self): estimator.run([(qc, op, param_list)] * k).result() self.assertEqual(run_mock.call_count, 10) + def test_iter_pub(self): + """test for an iterable of pubs""" + backend = BasicSimulator() + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + circuit = pm.run(circuit) + estimator = BackendEstimatorV2(backend=backend, options=self._options) + observable = self.observable.apply_layout(circuit.layout) + result = estimator.run(iter([(circuit, observable), (circuit, observable)])).result() + 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) + 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 64a26471a33..dd58920689a 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -733,6 +733,23 @@ def test_job_size_limit_backend_v1(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_iter_pub(self): + """Test of an iterable of pubs""" + backend = BasicSimulator() + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + sampler = BackendSamplerV2(backend=backend) + result = sampler.run(iter([qc, qc2]), shots=self._shots).result() + self.assertIsInstance(result, PrimitiveResult) + self.assertEqual(len(result), 2) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[1], PubResult) + self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) + self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 15c022f770c..117ead6717a 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -281,6 +281,15 @@ def test_precision_seed(self): result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956]) + def test_iter_pub(self): + """test for an iterable of pubs""" + estimator = StatevectorEstimator() + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + observable = self.observable.apply_layout(circuit.layout) + result = estimator.run(iter([(circuit, observable), (circuit, observable)])).result() + np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733]) + np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733]) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index cd0622b18de..1a8ed0402e5 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -621,6 +621,22 @@ def test_no_cregs(self): self.assertEqual(len(result), 1) self.assertEqual(len(result[0].data), 0) + def test_iter_pub(self): + """Test of an iterable of pubs""" + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + sampler = StatevectorSampler() + result = sampler.run(iter([qc, qc2]), shots=self._shots).result() + self.assertIsInstance(result, PrimitiveResult) + self.assertEqual(len(result), 2) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[1], PubResult) + self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) + self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + if __name__ == "__main__": unittest.main() From febc16cb43ef7d9663c6363e9b92d3cca9036115 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Apr 2024 17:28:46 -0400 Subject: [PATCH 010/159] Oxidize the numeric code in the Isometry gate class (#12197) * Oxidize the numeric code in the Isometry gate class This commit ports the numeric portion of the Isometry gate class to rust. While this will likely improve the performance slightly this move is more to make isolate this code from blas/lapack in numpy. We're hitting some stability issues on arm64 mac in CI and moving this code to rust should hopefully fix this issue. As this is more for functional reasons no real performance tuning was done on this port, there are likely several opportunities to improve the runtime performance of the code. * Remove unused import * Use rust impl for small utility functions too * Oxidize the linalg in UCGate too The UCGate class is used almost exclusively by the Isometry class to build up the definition of the isometry circuit. There were also some linear algebra inside the function which could also be the source of the stability issues we were seeing on arm64. This commit ports this function as part of the larger isometry migration. * Migrate another numeric helper method of UCGate * Remove now unused code paths * Remove bitstring usage with bitwise ops This commit removes the use of bit string manipulations that were faithfully ported from the original python logic (but left a bad taste in my mouth) into more efficient bitwise operations (which were possible in the original python too). * Mostly replace Vec usage with bitwise operations The use of intermediate Vec as proxy bitstrings was originally ported nearly exactly from the python implementation. But since everything is working now this commit switches to use bitwise operations where it makes sense as this will be more efficient. * Apply suggestions from code review Co-authored-by: Jake Lishman * Remove python side call sites * Fix integer typing in uc_gate.rs * Simplify basis state bitshift loop logic * Build set of control labels outside construct_basis_states * Use 2 element array for reverse_qubit_state * Micro optimize reverse_qubit_state * Use 1d numpy arrays for diagonal inputs * Fix lint * Update crates/accelerate/src/isometry.rs Co-authored-by: John Lapeyre * Add back sqrt() accidentally removed by inline suggestion * Use a constant for rz pi/2 elements --------- Co-authored-by: Jake Lishman Co-authored-by: John Lapeyre --- Cargo.lock | 10 + crates/accelerate/Cargo.toml | 1 + crates/accelerate/src/isometry.rs | 367 ++++++++++++++++++ crates/accelerate/src/lib.rs | 2 + crates/accelerate/src/two_qubit_decompose.rs | 2 +- crates/accelerate/src/uc_gate.rs | 163 ++++++++ crates/pyext/src/lib.rs | 11 +- qiskit/__init__.py | 2 + .../library/generalized_gates/isometry.py | 286 ++------------ .../circuit/library/generalized_gates/uc.py | 100 +---- 10 files changed, 596 insertions(+), 348 deletions(-) create mode 100644 crates/accelerate/src/isometry.rs create mode 100644 crates/accelerate/src/uc_gate.rs diff --git a/Cargo.lock b/Cargo.lock index 6859c622967..dee434e870a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,6 +589,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "jod-thread" version = "0.1.2" @@ -1079,6 +1088,7 @@ dependencies = [ "faer-ext", "hashbrown 0.14.3", "indexmap 2.2.6", + "itertools 0.12.1", "ndarray", "num-bigint", "num-complex", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 05ca3f5b639..a43fdc6ff50 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -21,6 +21,7 @@ num-complex = "0.4" num-bigint = "0.4" rustworkx-core = "0.14" faer = "0.18.2" +itertools = "0.12.1" qiskit-circuit.workspace = true [dependencies.smallvec] diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs new file mode 100644 index 00000000000..8d0761666bb --- /dev/null +++ b/crates/accelerate/src/isometry.rs @@ -0,0 +1,367 @@ +// 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 std::ops::BitAnd; + +use approx::abs_diff_eq; +use num_complex::{Complex64, ComplexFloat}; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use hashbrown::HashSet; +use itertools::Itertools; +use ndarray::prelude::*; +use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; + +use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY; + +/// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or +/// basis_state=1 respectively +#[pyfunction] +pub fn reverse_qubit_state( + py: Python, + state: [Complex64; 2], + basis_state: usize, + epsilon: f64, +) -> PyObject { + reverse_qubit_state_inner(&state, basis_state, epsilon) + .into_pyarray_bound(py) + .into() +} + +#[inline(always)] +fn l2_norm(vec: &[Complex64]) -> f64 { + vec.iter() + .fold(0., |acc, elem| acc + elem.norm_sqr()) + .sqrt() +} + +fn reverse_qubit_state_inner( + state: &[Complex64; 2], + basis_state: usize, + epsilon: f64, +) -> Array2 { + let r = l2_norm(state); + let r_inv = 1. / r; + if r < epsilon { + Array2::eye(2) + } else if basis_state == 0 { + array![ + [state[0].conj() * r_inv, state[1].conj() * r_inv], + [-state[1] * r_inv, state[0] * r_inv], + ] + } else { + array![ + [-state[1] * r_inv, state[0] * r_inv], + [state[0].conj() * r_inv, state[1].conj() * r_inv], + ] + } +} + +/// This method finds the single-qubit gates for a UCGate to disentangle a qubit: +/// we consider the n-qubit state v[:,0] starting with k zeros (in the computational basis). +/// The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). + +#[pyfunction] +pub fn find_squs_for_disentangling( + py: Python, + v: PyReadonlyArray2, + k: usize, + s: usize, + epsilon: f64, + n: usize, +) -> Vec { + let v = v.as_array(); + let k_prime = 0; + let i_start = if b(k, s + 1) == 0 { + a(k, s + 1) + } else { + a(k, s + 1) + 1 + }; + let mut output: Vec> = (0..i_start).map(|_| Array2::eye(2)).collect(); + let mut squs: Vec> = (i_start..2_usize.pow((n - s - 1) as u32)) + .map(|i| { + reverse_qubit_state_inner( + &[ + v[[2 * i * 2_usize.pow(s as u32) + b(k, s), k_prime]], + v[[(2 * i + 1) * 2_usize.pow(s as u32) + b(k, s), k_prime]], + ], + k_s(k, s), + epsilon, + ) + }) + .collect(); + output.append(&mut squs); + output + .into_iter() + .map(|x| x.into_pyarray_bound(py).into()) + .collect() +} + +#[pyfunction] +pub fn apply_ucg( + py: Python, + m: PyReadonlyArray2, + k: usize, + single_qubit_gates: Vec>, +) -> PyObject { + let mut m = m.as_array().to_owned(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + let spacing: usize = 2_usize.pow(num_qubits - k as u32 - 1); + for j in 0..2_usize.pow(num_qubits - 1) { + let i = (j / spacing) * spacing + j; + let gate_index = i / (2_usize.pow(num_qubits - k as u32)); + for col in 0..num_col { + let gate = single_qubit_gates[gate_index].as_array(); + let a = m[[i, col]]; + let b = m[[i + spacing, col]]; + m[[i, col]] = gate[[0, 0]] * a + gate[[0, 1]] * b; + m[[i + spacing, col]] = gate[[1, 0]] * a + gate[[1, 1]] * b; + } + } + m.into_pyarray_bound(py).into() +} + +#[inline(always)] +fn bin_to_int(bin: &[u8]) -> usize { + bin.iter() + .fold(0_usize, |acc, digit| (acc << 1) + *digit as usize) +} + +#[pyfunction] +pub fn apply_diagonal_gate( + py: Python, + m: PyReadonlyArray2, + action_qubit_labels: Vec, + diag: PyReadonlyArray1, +) -> PyResult { + let diag = diag.as_slice()?; + let mut m = m.as_array().to_owned(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + for state in std::iter::repeat([0_u8, 1_u8]) + .take(num_qubits as usize) + .multi_cartesian_product() + { + let diag_index = action_qubit_labels + .iter() + .fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); + let i = bin_to_int(&state); + for j in 0..num_col { + m[[i, j]] = diag[diag_index] * m[[i, j]] + } + } + Ok(m.into_pyarray_bound(py).into()) +} + +#[pyfunction] +pub fn apply_diagonal_gate_to_diag( + mut m_diagonal: Vec, + action_qubit_labels: Vec, + diag: PyReadonlyArray1, + num_qubits: usize, +) -> PyResult> { + let diag = diag.as_slice()?; + if m_diagonal.is_empty() { + return Ok(m_diagonal); + } + for state in std::iter::repeat([0_u8, 1_u8]) + .take(num_qubits) + .multi_cartesian_product() + .take(m_diagonal.len()) + { + let diag_index = action_qubit_labels + .iter() + .fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); + let i = bin_to_int(&state); + m_diagonal[i] *= diag[diag_index] + } + Ok(m_diagonal) +} + +/// Helper method for _apply_multi_controlled_gate. This constructs the basis states the MG gate +/// is acting on for a specific state state_free of the qubits we neither control nor act on +fn construct_basis_states( + state_free: &[u8], + control_set: &HashSet, + target_label: usize, +) -> [usize; 2] { + let size = state_free.len() + control_set.len() + 1; + let mut e1: usize = 0; + let mut e2: usize = 0; + let mut j = 0; + for i in 0..size { + e1 <<= 1; + e2 <<= 1; + if control_set.contains(&i) { + e1 += 1; + e2 += 1; + } else if i == target_label { + e2 += 1; + } else { + assert!(j <= 1); + e1 += state_free[j] as usize; + e2 += state_free[j] as usize; + j += 1 + } + } + [e1, e2] +} + +#[pyfunction] +pub fn apply_multi_controlled_gate( + py: Python, + m: PyReadonlyArray2, + control_labels: Vec, + target_label: usize, + gate: PyReadonlyArray2, +) -> PyObject { + let mut m = m.as_array().to_owned(); + let gate = gate.as_array(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + let free_qubits = num_qubits as usize - control_labels.len() - 1; + let control_set: HashSet = control_labels.into_iter().collect(); + if free_qubits == 0 { + let [e1, e2] = construct_basis_states(&[], &control_set, target_label); + for i in 0..num_col { + let temp: Vec<_> = gate + .dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) + .into_iter() + .take(2) + .collect(); + m[[e1, i]] = temp[0]; + m[[e2, i]] = temp[1]; + } + return m.into_pyarray_bound(py).into(); + } + for state_free in std::iter::repeat([0_u8, 1_u8]) + .take(free_qubits) + .multi_cartesian_product() + { + let [e1, e2] = construct_basis_states(&state_free, &control_set, target_label); + for i in 0..num_col { + let temp: Vec<_> = gate + .dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) + .into_iter() + .take(2) + .collect(); + m[[e1, i]] = temp[0]; + m[[e2, i]] = temp[1]; + } + } + m.into_pyarray_bound(py).into() +} + +#[pyfunction] +pub fn ucg_is_identity_up_to_global_phase( + single_qubit_gates: Vec>, + epsilon: f64, +) -> bool { + let global_phase: Complex64 = if single_qubit_gates[0].as_array()[[0, 0]].abs() >= epsilon { + single_qubit_gates[0].as_array()[[0, 0]].finv() + } else { + return false; + }; + for raw_gate in single_qubit_gates { + let gate = raw_gate.as_array(); + if !abs_diff_eq!( + gate.mapv(|x| x * global_phase), + aview2(&ONE_QUBIT_IDENTITY), + epsilon = 1e-8 // Default tolerance from numpy for allclose() + ) { + return false; + } + } + true +} + +#[pyfunction] +fn diag_is_identity_up_to_global_phase(diag: Vec, epsilon: f64) -> bool { + let global_phase: Complex64 = if diag[0].abs() >= epsilon { + diag[0].finv() + } else { + return false; + }; + for d in diag { + if (global_phase * d - 1.0).abs() >= epsilon { + return false; + } + } + true +} + +#[pyfunction] +pub fn merge_ucgate_and_diag( + py: Python, + single_qubit_gates: Vec>, + diag: Vec, +) -> Vec { + single_qubit_gates + .iter() + .enumerate() + .map(|(i, raw_gate)| { + let gate = raw_gate.as_array(); + let res = aview2(&[ + [diag[2 * i], Complex64::new(0., 0.)], + [Complex64::new(0., 0.), diag[2 * i + 1]], + ]) + .dot(&gate); + res.into_pyarray_bound(py).into() + }) + .collect() +} + +#[inline(always)] +#[pyfunction] +fn k_s(k: usize, s: usize) -> usize { + if k == 0 { + 0 + } else { + let filter = 1 << s; + k.bitand(filter) >> s + } +} + +#[inline(always)] +#[pyfunction] +fn a(k: usize, s: usize) -> usize { + k / 2_usize.pow(s as u32) +} + +#[inline(always)] +#[pyfunction] +fn b(k: usize, s: usize) -> usize { + k - (a(k, s) * 2_usize.pow(s as u32)) +} + +#[pymodule] +pub fn isometry(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(diag_is_identity_up_to_global_phase))?; + m.add_wrapped(wrap_pyfunction!(find_squs_for_disentangling))?; + m.add_wrapped(wrap_pyfunction!(reverse_qubit_state))?; + m.add_wrapped(wrap_pyfunction!(apply_ucg))?; + m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate))?; + m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate_to_diag))?; + m.add_wrapped(wrap_pyfunction!(apply_multi_controlled_gate))?; + m.add_wrapped(wrap_pyfunction!(ucg_is_identity_up_to_global_phase))?; + m.add_wrapped(wrap_pyfunction!(merge_ucgate_and_diag))?; + m.add_wrapped(wrap_pyfunction!(a))?; + m.add_wrapped(wrap_pyfunction!(b))?; + m.add_wrapped(wrap_pyfunction!(k_s))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index bb7621dce34..0af8ea6a0fc 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -19,6 +19,7 @@ pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; @@ -28,6 +29,7 @@ pub mod sampled_exp_val; pub mod sparse_pauli_op; pub mod stochastic_swap; pub mod two_qubit_decompose; +pub mod uc_gate; pub mod utils; pub mod vf2_layout; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7dcb273ac16..5e833bd86fd 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -60,7 +60,7 @@ const TWO_PI: f64 = 2.0 * PI; const C1: c64 = c64 { re: 1.0, im: 0.0 }; -static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ +pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ [Complex64::new(1., 0.), Complex64::new(0., 0.)], [Complex64::new(0., 0.), Complex64::new(1., 0.)], ]; diff --git a/crates/accelerate/src/uc_gate.rs b/crates/accelerate/src/uc_gate.rs new file mode 100644 index 00000000000..3a5f74a6f0b --- /dev/null +++ b/crates/accelerate/src/uc_gate.rs @@ -0,0 +1,163 @@ +// 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 num_complex::{Complex64, ComplexFloat}; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; +use std::f64::consts::{FRAC_1_SQRT_2, PI}; + +use faer_ext::{IntoFaerComplex, IntoNdarrayComplex}; +use ndarray::prelude::*; +use numpy::{IntoPyArray, PyReadonlyArray2}; + +use crate::euler_one_qubit_decomposer::det_one_qubit; + +const PI2: f64 = PI / 2.; +const EPS: f64 = 1e-10; + +// These constants are the non-zero elements of an RZ gate's unitary with an +// angle of pi / 2 +const RZ_PI2_11: Complex64 = Complex64::new(FRAC_1_SQRT_2, -FRAC_1_SQRT_2); +const RZ_PI2_00: Complex64 = Complex64::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2); + +/// This method implements the decomposition given in equation (3) in +/// https://arxiv.org/pdf/quant-ph/0410066.pdf. +/// +/// The decomposition is used recursively to decompose uniformly controlled gates. +/// +/// a,b = single qubit unitaries +/// v,u,r = outcome of the decomposition given in the reference mentioned above +/// +/// (see there for the details). +fn demultiplex_single_uc( + a: ArrayView2, + b: ArrayView2, +) -> [Array2; 3] { + let x = a.dot(&b.mapv(|x| x.conj()).t()); + let det_x = det_one_qubit(x.view()); + let x11 = x[[0, 0]] / det_x.sqrt(); + let phi = det_x.arg(); + + let r1 = (Complex64::new(0., 1.) / 2. * (PI2 - phi / 2. - x11.arg())).exp(); + let r2 = (Complex64::new(0., 1.) / 2. * (PI2 - phi / 2. + x11.arg() + PI)).exp(); + + let r = array![[r1, Complex64::new(0., 0.)], [Complex64::new(0., 0.), r2],]; + + let decomp = r + .dot(&x) + .dot(&r) + .view() + .into_faer_complex() + .complex_eigendecomposition(); + let mut u: Array2 = decomp.u().into_ndarray_complex().to_owned(); + let s = decomp.s().column_vector(); + let mut diag: Array1 = + Array1::from_shape_fn(u.shape()[0], |i| s[i].to_num_complex()); + + // If d is not equal to diag(i,-i), then we put it into this "standard" form + // (see eq. (13) in https://arxiv.org/pdf/quant-ph/0410066.pdf) by interchanging + // the eigenvalues and eigenvectors + if (diag[0] + Complex64::new(0., 1.)).abs() < EPS { + diag = diag.slice(s![..;-1]).to_owned(); + u = u.slice(s![.., ..;-1]).to_owned(); + } + diag.mapv_inplace(|x| x.sqrt()); + let d = Array2::from_diag(&diag); + let v = d + .dot(&u.mapv(|x| x.conj()).t()) + .dot(&r.mapv(|x| x.conj()).t()) + .dot(&b); + [v, u, r] +} + +#[pyfunction] +pub fn dec_ucg_help( + py: Python, + sq_gates: Vec>, + num_qubits: u32, +) -> (Vec, PyObject) { + let mut single_qubit_gates: Vec> = sq_gates + .into_iter() + .map(|x| x.as_array().to_owned()) + .collect(); + let mut diag: Array1 = Array1::ones(2_usize.pow(num_qubits)); + let num_controls = num_qubits - 1; + for dec_step in 0..num_controls { + let num_ucgs = 2_usize.pow(dec_step); + // The decomposition works recursively and the followign loop goes over the different + // UCGates that arise in the decomposition + for ucg_index in 0..num_ucgs { + let len_ucg = 2_usize.pow(num_controls - dec_step); + for i in 0..len_ucg / 2 { + let shift = ucg_index * len_ucg; + let a = single_qubit_gates[shift + i].view(); + let b = single_qubit_gates[shift + len_ucg / 2 + i].view(); + // Apply the decomposition for UCGates given in equation (3) in + // https://arxiv.org/pdf/quant-ph/0410066.pdf + // to demultiplex one control of all the num_ucgs uniformly-controlled gates + // with log2(len_ucg) uniform controls + let [v, u, r] = demultiplex_single_uc(a, b); + // replace the single-qubit gates with v,u (the already existing ones + // are not needed any more) + single_qubit_gates[shift + i] = v; + single_qubit_gates[shift + len_ucg / 2 + i] = u; + // Now we decompose the gates D as described in Figure 4 in + // https://arxiv.org/pdf/quant-ph/0410066.pdf and merge some of the gates + // into the UCGates and the diagonal at the end of the circuit + + // Remark: The Rz(pi/2) rotation acting on the target qubit and the Hadamard + // gates arising in the decomposition of D are ignored for the moment (they will + // be added together with the C-NOT gates at the end of the decomposition + // (in the method dec_ucg())) + let r_conj_t = r.mapv(|x| x.conj()).t().to_owned(); + if ucg_index < num_ucgs - 1 { + // Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and + // merge the UC-Rz rotation with the following UCGate, + // which hasn't been decomposed yet + let k = shift + len_ucg + i; + + single_qubit_gates[k] = single_qubit_gates[k].dot(&r_conj_t); + single_qubit_gates[k].mapv_inplace(|x| x * RZ_PI2_00); + let k = k + len_ucg / 2; + single_qubit_gates[k] = single_qubit_gates[k].dot(&r); + single_qubit_gates[k].mapv_inplace(|x| x * RZ_PI2_11); + } else { + // Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and merge + // the trailing UC-Rz rotation into a diagonal gate at the end of the circuit + for ucg_index_2 in 0..num_ucgs { + let shift_2 = ucg_index_2 * len_ucg; + let k = 2 * (i + shift_2); + diag[k] *= r_conj_t[[0, 0]] * RZ_PI2_00; + diag[k + 1] *= r_conj_t[[1, 1]] * RZ_PI2_00; + let k = len_ucg + k; + diag[k] *= r[[0, 0]] * RZ_PI2_11; + diag[k + 1] *= r[[1, 1]] * RZ_PI2_11; + } + } + } + } + } + ( + single_qubit_gates + .into_iter() + .map(|x| x.into_pyarray_bound(py).into()) + .collect(), + diag.into_pyarray_bound(py).into(), + ) +} + +#[pymodule] +pub fn uc_gate(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(dec_ucg_help))?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index b7c89872bf8..a21b1307a88 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -15,10 +15,11 @@ use pyo3::wrap_pymodule; use qiskit_accelerate::{ convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, - error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, 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, two_qubit_decompose::two_qubit_decompose, utils::utils, + 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, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, vf2_layout::vf2_layout, }; @@ -31,6 +32,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(dense_layout))?; m.add_wrapped(wrap_pymodule!(error_map))?; m.add_wrapped(wrap_pymodule!(euler_one_qubit_decomposer))?; + m.add_wrapped(wrap_pymodule!(isometry))?; m.add_wrapped(wrap_pymodule!(nlayout))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(pauli_expval))?; @@ -40,6 +42,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(sparse_pauli_op))?; m.add_wrapped(wrap_pymodule!(stochastic_swap))?; m.add_wrapped(wrap_pymodule!(two_qubit_decompose))?; + m.add_wrapped(wrap_pymodule!(uc_gate))?; m.add_wrapped(wrap_pymodule!(utils))?; m.add_wrapped(wrap_pymodule!(vf2_layout))?; Ok(()) diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 590a5698a77..e4fbc1729e5 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -65,6 +65,8 @@ ) sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout sys.modules["qiskit._accelerate.error_map"] = qiskit._accelerate.error_map +sys.modules["qiskit._accelerate.isometry"] = qiskit._accelerate.isometry +sys.modules["qiskit._accelerate.uc_gate"] = qiskit._accelerate.uc_gate sys.modules["qiskit._accelerate.euler_one_qubit_decomposer"] = ( qiskit._accelerate.euler_one_qubit_decomposer ) diff --git a/qiskit/circuit/library/generalized_gates/isometry.py b/qiskit/circuit/library/generalized_gates/isometry.py index 1294feb2634..c180e7a1448 100644 --- a/qiskit/circuit/library/generalized_gates/isometry.py +++ b/qiskit/circuit/library/generalized_gates/isometry.py @@ -21,7 +21,6 @@ from __future__ import annotations -import itertools import math import numpy as np from qiskit.circuit.exceptions import CircuitError @@ -30,6 +29,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_isometry +from qiskit._accelerate import isometry as isometry_rs from .diagonal import Diagonal from .uc import UCGate @@ -157,12 +157,16 @@ def _gates_to_uncompute(self): # correspond to the firstfew columns of the identity matrix up to diag, and hence we only # have to save a list containing them. for column_index in range(2**m): - self._decompose_column(circuit, q, diag, remaining_isometry, column_index) + remaining_isometry, diag = self._decompose_column( + circuit, q, diag, remaining_isometry, column_index + ) # extract phase of the state that was sent to the basis state ket(column_index) diag.append(remaining_isometry[column_index, 0]) # remove first column (which is now stored in diag) remaining_isometry = remaining_isometry[:, 1:] - if len(diag) > 1 and not _diag_is_identity_up_to_global_phase(diag, self._epsilon): + if len(diag) > 1 and not isometry_rs.diag_is_identity_up_to_global_phase( + diag, self._epsilon + ): diagonal = Diagonal(np.conj(diag)) circuit.append(diagonal, q_input) return circuit @@ -173,7 +177,10 @@ def _decompose_column(self, circuit, q, diag, remaining_isometry, column_index): """ n = int(math.log2(self.iso_data.shape[0])) for s in range(n): - self._disentangle(circuit, q, diag, remaining_isometry, column_index, s) + remaining_isometry, diag = self._disentangle( + circuit, q, diag, remaining_isometry, column_index, s + ) + return remaining_isometry, diag def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): """ @@ -189,13 +196,19 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): n = int(math.log2(self.iso_data.shape[0])) # MCG to set one entry to zero (preparation for disentangling with UCGate): - index1 = 2 * _a(k, s + 1) * 2**s + _b(k, s + 1) - index2 = (2 * _a(k, s + 1) + 1) * 2**s + _b(k, s + 1) + index1 = 2 * isometry_rs.a(k, s + 1) * 2**s + isometry_rs.b(k, s + 1) + index2 = (2 * isometry_rs.a(k, s + 1) + 1) * 2**s + isometry_rs.b(k, s + 1) target_label = n - s - 1 # Check if a MCG is required - if _k_s(k, s) == 0 and _b(k, s + 1) != 0 and np.abs(v[index2, k_prime]) > self._epsilon: + if ( + isometry_rs.k_s(k, s) == 0 + and isometry_rs.b(k, s + 1) != 0 + and np.abs(v[index2, k_prime]) > self._epsilon + ): # Find the MCG, decompose it and apply it to the remaining isometry - gate = _reverse_qubit_state([v[index1, k_prime], v[index2, k_prime]], 0, self._epsilon) + gate = isometry_rs.reverse_qubit_state( + [v[index1, k_prime], v[index2, k_prime]], 0, self._epsilon + ) control_labels = [ i for i, x in enumerate(_get_binary_rep_as_list(k, n)) @@ -205,57 +218,49 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): circuit, q, gate, control_labels, target_label ) # apply the MCG to the remaining isometry - _apply_multi_controlled_gate(v, control_labels, target_label, gate) + v = isometry_rs.apply_multi_controlled_gate(v, control_labels, target_label, gate) # correct for the implementation "up to diagonal" - diag_mcg_inverse = np.conj(diagonal_mcg).tolist() - _apply_diagonal_gate(v, control_labels + [target_label], diag_mcg_inverse) + diag_mcg_inverse = np.conj(diagonal_mcg).astype(complex, copy=False) + v = isometry_rs.apply_diagonal_gate( + v, control_labels + [target_label], diag_mcg_inverse + ) # update the diag according to the applied diagonal gate - _apply_diagonal_gate_to_diag(diag, control_labels + [target_label], diag_mcg_inverse, n) + diag = isometry_rs.apply_diagonal_gate_to_diag( + diag, control_labels + [target_label], diag_mcg_inverse, n + ) # UCGate to disentangle a qubit: # Find the UCGate, decompose it and apply it to the remaining isometry single_qubit_gates = self._find_squs_for_disentangling(v, k, s) - if not _ucg_is_identity_up_to_global_phase(single_qubit_gates, self._epsilon): + if not isometry_rs.ucg_is_identity_up_to_global_phase(single_qubit_gates, self._epsilon): control_labels = list(range(target_label)) diagonal_ucg = self._append_ucg_up_to_diagonal( circuit, q, single_qubit_gates, control_labels, target_label ) # merge the diagonal into the UCGate for efficient application of both together - diagonal_ucg_inverse = np.conj(diagonal_ucg).tolist() - single_qubit_gates = _merge_UCGate_and_diag(single_qubit_gates, diagonal_ucg_inverse) + diagonal_ucg_inverse = np.conj(diagonal_ucg).astype(complex, copy=False) + single_qubit_gates = isometry_rs.merge_ucgate_and_diag( + single_qubit_gates, diagonal_ucg_inverse + ) # apply the UCGate (with the merged diagonal gate) to the remaining isometry - _apply_ucg(v, len(control_labels), single_qubit_gates) + v = isometry_rs.apply_ucg(v, len(control_labels), single_qubit_gates) # update the diag according to the applied diagonal gate - _apply_diagonal_gate_to_diag( + diag = isometry_rs.apply_diagonal_gate_to_diag( diag, control_labels + [target_label], diagonal_ucg_inverse, n ) # # correct for the implementation "up to diagonal" # diag_inv = np.conj(diag).tolist() # _apply_diagonal_gate(v, control_labels + [target_label], diag_inv) + return v, diag # This method finds the single-qubit gates for a UCGate to disentangle a qubit: # we consider the n-qubit state v[:,0] starting with k zeros (in the computational basis). # The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). def _find_squs_for_disentangling(self, v, k, s): - k_prime = 0 - n = int(math.log2(self.iso_data.shape[0])) - if _b(k, s + 1) == 0: - i_start = _a(k, s + 1) - else: - i_start = _a(k, s + 1) + 1 - id_list = [np.eye(2, 2) for _ in range(i_start)] - squs = [ - _reverse_qubit_state( - [ - v[2 * i * 2**s + _b(k, s), k_prime], - v[(2 * i + 1) * 2**s + _b(k, s), k_prime], - ], - _k_s(k, s), - self._epsilon, - ) - for i in range(i_start, 2 ** (n - s - 1)) - ] - return id_list + squs + res = isometry_rs.find_squs_for_disentangling( + v, k, s, self._epsilon, n=int(math.log2(self.iso_data.shape[0])) + ) + return res # Append a UCGate up to diagonal to the circuit circ. def _append_ucg_up_to_diagonal(self, circ, q, single_qubit_gates, control_labels, target_label): @@ -338,146 +343,6 @@ def inv_gate(self): return self._inverse -# Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or -# basis_state=1 respectively -def _reverse_qubit_state(state, basis_state, epsilon): - state = np.array(state) - r = np.linalg.norm(state) - if r < epsilon: - return np.eye(2, 2) - if basis_state == 0: - m = np.array([[np.conj(state[0]), np.conj(state[1])], [-state[1], state[0]]]) / r - else: - m = np.array([[-state[1], state[0]], [np.conj(state[0]), np.conj(state[1])]]) / r - return m - - -# Methods for applying gates to matrices (should be moved to Qiskit AER) - -# Input: matrix m with 2^n rows (and arbitrary many columns). Think of the columns as states -# on n qubits. The method applies a uniformly controlled gate (UCGate) to all the columns, where -# the UCGate is specified by the inputs k and single_qubit_gates: - -# k = number of controls. We assume that the controls are on the k most significant qubits -# (and the target is on the (k+1)th significant qubit) -# single_qubit_gates = [u_0,...,u_{2^k-1}], where the u_i's are 2*2 unitaries -# (provided as numpy arrays) - -# The order of the single-qubit unitaries is such that the first unitary u_0 is applied to the -# (k+1)th significant qubit if the control qubits are in the state ket(0...00), the gate u_1 is -# applied if the control qubits are in the state ket(0...01), and so on. - -# The input matrix m and the single-qubit gates have to be of dtype=complex. - - -def _apply_ucg(m, k, single_qubit_gates): - # ToDo: Improve efficiency by parallelizing the gate application. A generalized version of - # ToDo: this method should be implemented by the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_col = m.shape[1] - spacing = 2 ** (num_qubits - k - 1) - for j in range(2 ** (num_qubits - 1)): - i = (j // spacing) * spacing + j - gate_index = i // (2 ** (num_qubits - k)) - for col in range(num_col): - m[np.array([i, i + spacing]), np.array([col, col])] = np.ndarray.flatten( - single_qubit_gates[gate_index].dot(np.array([[m[i, col]], [m[i + spacing, col]]])) - ).tolist() - return m - - -# Apply a diagonal gate with diagonal entries liste in diag and acting on qubits with labels -# action_qubit_labels to a matrix m. -# The input matrix m has to be of dtype=complex -# The qubit labels are such that label 0 corresponds to the most significant qubit, label 1 to -# the second most significant qubit, and so on ... - - -def _apply_diagonal_gate(m, action_qubit_labels, diag): - # ToDo: Improve efficiency by parallelizing the gate application. A generalized version of - # ToDo: this method should be implemented by the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_cols = m.shape[1] - basis_states = list(itertools.product([0, 1], repeat=num_qubits)) - for state in basis_states: - state_on_action_qubits = [state[i] for i in action_qubit_labels] - diag_index = _bin_to_int(state_on_action_qubits) - i = _bin_to_int(state) - for j in range(num_cols): - m[i, j] = diag[diag_index] * m[i, j] - return m - - -# Special case of the method _apply_diagonal_gate, where the input m is a diagonal matrix on the -# log2(len(m_diagonal)) least significant qubits (this method is more efficient in this case -# than _apply_diagonal_gate). The input m_diagonal is provided as a list of diagonal entries. -# The diagonal diag is applied on the qubits with labels listed in action_qubit_labels. The input -# num_qubits gives the total number of considered qubits (this input is required to interpret the -# action_qubit_labels in relation to the least significant qubits). - - -def _apply_diagonal_gate_to_diag(m_diagonal, action_qubit_labels, diag, num_qubits): - if not m_diagonal: - return m_diagonal - basis_states = list(itertools.product([0, 1], repeat=num_qubits)) - for state in basis_states[: len(m_diagonal)]: - state_on_action_qubits = [state[i] for i in action_qubit_labels] - diag_index = _bin_to_int(state_on_action_qubits) - i = _bin_to_int(state) - m_diagonal[i] *= diag[diag_index] - return m_diagonal - - -# Apply a MC single-qubit gate (given by the 2*2 unitary input: gate) with controlling on -# the qubits with label control_labels and acting on the qubit with label target_label -# to a matrix m. The input matrix m and the gate have to be of dtype=complex. The qubit labels are -# such that label 0 corresponds to the most significant qubit, label 1 to the second most -# significant qubit, and so on ... - - -def _apply_multi_controlled_gate(m, control_labels, target_label, gate): - # ToDo: This method should be integrated into the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_cols = m.shape[1] - control_labels.sort() - free_qubits = num_qubits - len(control_labels) - 1 - basis_states_free = list(itertools.product([0, 1], repeat=free_qubits)) - for state_free in basis_states_free: - (e1, e2) = _construct_basis_states(state_free, control_labels, target_label) - for i in range(num_cols): - m[np.array([e1, e2]), np.array([i, i])] = np.ndarray.flatten( - gate.dot(np.array([[m[e1, i]], [m[e2, i]]])) - ).tolist() - return m - - -# Helper method for _apply_multi_controlled_gate. This constructs the basis states the MG gate -# is acting on for a specific state state_free of the qubits we neither control nor act on. - - -def _construct_basis_states(state_free, control_labels, target_label): - e1 = [] - e2 = [] - j = 0 - for i in range(len(state_free) + len(control_labels) + 1): - if i in control_labels: - e1.append(1) - e2.append(1) - elif i == target_label: - e1.append(0) - e2.append(1) - else: - e1.append(state_free[j]) - e2.append(state_free[j]) - j += 1 - out1 = _bin_to_int(e1) - out2 = _bin_to_int(e2) - return out1, out2 - - -# Some helper methods: - - # Get the qubits in the list qubits corresponding to the labels listed in labels. The total number # of qubits is given by num_qubits (and determines the convention for the qubit labeling) @@ -496,14 +361,6 @@ def _reverse_qubit_oder(qubits): # Convert list of binary digits to integer -def _bin_to_int(binary_digits_as_list): - return int("".join(str(x) for x in binary_digits_as_list), 2) - - -def _ct(m): - return np.transpose(np.conjugate(m)) - - def _get_binary_rep_as_list(n, num_digits): binary_string = np.binary_repr(n).zfill(num_digits) binary = [] @@ -511,64 +368,3 @@ def _get_binary_rep_as_list(n, num_digits): for c in line: binary.append(int(c)) return binary[-num_digits:] - - -# absorb a diagonal gate into a UCGate - - -def _merge_UCGate_and_diag(single_qubit_gates, diag): - for i, gate in enumerate(single_qubit_gates): - single_qubit_gates[i] = np.array([[diag[2 * i], 0.0], [0.0, diag[2 * i + 1]]]).dot(gate) - return single_qubit_gates - - -# Helper variables/functions for the column-by-column decomposition - - -# a(k,s) and b(k,s) are positive integers such that k = a(k,s)2^s + b(k,s) -# (with the maximal choice of a(k,s)) - - -def _a(k, s): - return k // 2**s - - -def _b(k, s): - return k - (_a(k, s) * 2**s) - - -# given a binary representation of k with binary digits [k_{n-1},..,k_1,k_0], -# the method k_s(k, s) returns k_s - - -def _k_s(k, s): - if k == 0: - return 0 - else: - num_digits = s + 1 - return _get_binary_rep_as_list(k, num_digits)[0] - - -# Check if a gate of a special form is equal to the identity gate up to global phase - - -def _ucg_is_identity_up_to_global_phase(single_qubit_gates, epsilon): - if not np.abs(single_qubit_gates[0][0, 0]) < epsilon: - global_phase = 1.0 / (single_qubit_gates[0][0, 0]) - else: - return False - for gate in single_qubit_gates: - if not np.allclose(global_phase * gate, np.eye(2, 2)): - return False - return True - - -def _diag_is_identity_up_to_global_phase(diag, epsilon): - if not np.abs(diag[0]) < epsilon: - global_phase = 1.0 / (diag[0]) - else: - return False - for d in diag: - if not np.abs(global_phase * d - 1) < epsilon: - return False - return True diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index 2d650e98466..f54567123e0 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -21,7 +21,6 @@ from __future__ import annotations -import cmath import math import numpy as np @@ -33,14 +32,11 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError from qiskit.exceptions import QiskitError - -# pylint: disable=cyclic-import -from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer +from qiskit._accelerate import uc_gate from .diagonal import Diagonal _EPS = 1e-10 # global variable used to chop very small numbers to zero -_DECOMPOSER1Q = OneQubitEulerDecomposer("U3") class UCGate(Gate): @@ -203,99 +199,7 @@ def _dec_ucg_help(self): https://arxiv.org/pdf/quant-ph/0410066.pdf. """ single_qubit_gates = [gate.astype(complex) for gate in self.params] - diag = np.ones(2**self.num_qubits, dtype=complex) - num_contr = self.num_qubits - 1 - for dec_step in range(num_contr): - num_ucgs = 2**dec_step - # The decomposition works recursively and the following loop goes over the different - # UCGates that arise in the decomposition - for ucg_index in range(num_ucgs): - len_ucg = 2 ** (num_contr - dec_step) - for i in range(int(len_ucg / 2)): - shift = ucg_index * len_ucg - a = single_qubit_gates[shift + i] - b = single_qubit_gates[shift + len_ucg // 2 + i] - # Apply the decomposition for UCGates given in equation (3) in - # https://arxiv.org/pdf/quant-ph/0410066.pdf - # to demultiplex one control of all the num_ucgs uniformly-controlled gates - # with log2(len_ucg) uniform controls - v, u, r = self._demultiplex_single_uc(a, b) - # replace the single-qubit gates with v,u (the already existing ones - # are not needed any more) - single_qubit_gates[shift + i] = v - single_qubit_gates[shift + len_ucg // 2 + i] = u - # Now we decompose the gates D as described in Figure 4 in - # https://arxiv.org/pdf/quant-ph/0410066.pdf and merge some of the gates - # into the UCGates and the diagonal at the end of the circuit - - # Remark: The Rz(pi/2) rotation acting on the target qubit and the Hadamard - # gates arising in the decomposition of D are ignored for the moment (they will - # be added together with the C-NOT gates at the end of the decomposition - # (in the method dec_ucg())) - if ucg_index < num_ucgs - 1: - # Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and - # merge the UC-Rz rotation with the following UCGate, - # which hasn't been decomposed yet. - k = shift + len_ucg + i - single_qubit_gates[k] = single_qubit_gates[k].dot( - UCGate._ct(r) - ) * UCGate._rz(np.pi / 2).item((0, 0)) - k = k + len_ucg // 2 - single_qubit_gates[k] = single_qubit_gates[k].dot(r) * UCGate._rz( - np.pi / 2 - ).item((1, 1)) - else: - # Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and merge - # the trailing UC-Rz rotation into a diagonal gate at the end of the circuit - for ucg_index_2 in range(num_ucgs): - shift_2 = ucg_index_2 * len_ucg - k = 2 * (i + shift_2) - diag[k] = ( - diag[k] - * UCGate._ct(r).item((0, 0)) - * UCGate._rz(np.pi / 2).item((0, 0)) - ) - diag[k + 1] = ( - diag[k + 1] - * UCGate._ct(r).item((1, 1)) - * UCGate._rz(np.pi / 2).item((0, 0)) - ) - k = len_ucg + k - diag[k] *= r.item((0, 0)) * UCGate._rz(np.pi / 2).item((1, 1)) - diag[k + 1] *= r.item((1, 1)) * UCGate._rz(np.pi / 2).item((1, 1)) - return single_qubit_gates, diag - - def _demultiplex_single_uc(self, a, b): - """ - This method implements the decomposition given in equation (3) in - https://arxiv.org/pdf/quant-ph/0410066.pdf. - The decomposition is used recursively to decompose uniformly controlled gates. - a,b = single qubit unitaries - v,u,r = outcome of the decomposition given in the reference mentioned above - (see there for the details). - """ - # The notation is chosen as in https://arxiv.org/pdf/quant-ph/0410066.pdf. - x = a.dot(UCGate._ct(b)) - det_x = np.linalg.det(x) - x11 = x.item((0, 0)) / cmath.sqrt(det_x) - phi = cmath.phase(det_x) - r1 = cmath.exp(1j / 2 * (np.pi / 2 - phi / 2 - cmath.phase(x11))) - r2 = cmath.exp(1j / 2 * (np.pi / 2 - phi / 2 + cmath.phase(x11) + np.pi)) - r = np.array([[r1, 0], [0, r2]], dtype=complex) - d, u = np.linalg.eig(r.dot(x).dot(r)) - # If d is not equal to diag(i,-i), then we put it into this "standard" form - # (see eq. (13) in https://arxiv.org/pdf/quant-ph/0410066.pdf) by interchanging - # the eigenvalues and eigenvectors. - if abs(d[0] + 1j) < _EPS: - d = np.flip(d, 0) - u = np.flip(u, 1) - d = np.diag(np.sqrt(d)) - v = d.dot(UCGate._ct(u)).dot(UCGate._ct(r)).dot(b) - return v, u, r - - @staticmethod - def _ct(m): - return np.transpose(np.conjugate(m)) + return uc_gate.dec_ucg_help(single_qubit_gates, self.num_qubits) @staticmethod def _rz(alpha): From f2b874b83c30df6bd08aa2c13399f9f8020b7a9a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Apr 2024 19:24:28 -0400 Subject: [PATCH 011/159] Promote arm64 macOS to tier 1 (#12102) * Promote arm64 macOS to tier 1 Github recently added a new macOS runner that is using the m1 CPU that is usable for open source projects. [1] Previously Qiskit had support for arm64 macOS at tier 3 because we were only able to cross compile for the platform and not test the binaries. Now that we can run CI jobs on the platform we're able to run both unit tests and test our binaries on release. This commit adds a new set of test jobs and wheel builds that use the macos-14 runner that mirrors the existing x86_64 macOS jobs we have. This brings arm64 macOS to the same support level as x86_64. THe only difference here is that azure pipelines doesn't have arm64 macOS support so the test job will run in github actions instead. [1] https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/ * Update test job * Fix syntax error * Use a string for python version in test job * Disable fail-fast on test job * DNM: Test wheel builds * Skip universal builds * Force system python to arm64 on macOS arm64 bwheel builds * Correctly skip universal builds * Skip python 3.8 arm64 * Add back cross build job just for python 3.8 * Add numpy env details to arm64 test job * Use MSRV for one of the test jobs * Fix build_pgo.sh script when running on arm64 mac * Revert "DNM: Test wheel builds" This reverts commit 97eaa6fee84d8209e530e59017478c78bce1a868. * Rename macos arm py38 cross-build job --- .github/workflows/tests.yml | 44 +++++++++++++++++++ .github/workflows/wheels.yml | 43 +++++++++++------- pyproject.toml | 2 +- .../macos-arm64-tier-1-c5030f009be6adcb.yaml | 11 +++++ tools/build_pgo.sh | 8 +++- 5 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000000..0d04e21a169 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,44 @@ +--- +name: Tests +on: + push: + branches: [ main, 'stable/*' ] + pull_request: + branches: [ main, 'stable/*' ] + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} + cancel-in-progress: true +jobs: + tests: + if: github.repository_owner == 'Qiskit' + name: macOS-arm64-tests-Python-${{ matrix.python-version }} + runs-on: macOS-14 + strategy: + fail-fast: false + matrix: + # Normally we test min and max version but we can't run python 3.8 or + # 3.9 on arm64 until actions/setup-python#808 is resolved + python-version: ["3.10", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.70 + if: matrix.python-version == '3.10' + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: arm64 + - name: 'Install dependencies' + run: | + python -m pip install -U -r requirements.txt -c constraints.txt + python -m pip install -U -r requirements-dev.txt -c constraints.txt + python -m pip install -c constraints.txt -e . + - name: 'Install optionals' + run: | + python -m pip install -r requirements-optional.txt -c constraints.txt + python tools/report_numpy_state.py + if: matrix.python-version == '3.10' + - name: 'Run tests' + run: stestr run diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 67104433a3d..2cd3c8ac0a3 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,13 +12,20 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-11, windows-latest] + os: [ubuntu-latest, macos-11, windows-latest, macos-14] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.10' + if: matrix.os != 'macos-14' + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: '3.10' + architecture: arm64 + if: matrix.os == 'macos-14' - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview @@ -34,13 +41,13 @@ jobs: with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }} - build_wheels_32bit: - name: Build wheels 32bit + build_wheels_macos_arm_py38: + name: Build wheels on macOS arm runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [macos-11] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -53,19 +60,23 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' + CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin + CIBW_BUILD: cp38-macosx_universal2 cp38-macosx_arm64 + CIBW_ARCHS_MACOS: arm64 universal2 + CIBW_ENVIRONMENT: >- + CARGO_BUILD_TARGET="aarch64-apple-darwin" + PYO3_CROSS_LIB_DIR="/Library/Frameworks/Python.framework/Versions/$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')/lib/python$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')" - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl - name: wheels-${{ matrix.os }}-32 - build_wheels_macos_arm: - name: Build wheels on macOS arm + name: wheels-${{ matrix.os }}-arm + build_wheels_32bit: + name: Build wheels 32bit runs-on: ${{ matrix.os }} - environment: release strategy: fail-fast: false matrix: - os: [macos-11] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -73,25 +84,23 @@ jobs: with: python-version: '3.10' - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview - name: Build wheels uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin - CIBW_ARCHS_MACOS: arm64 universal2 - CIBW_ENVIRONMENT: >- - CARGO_BUILD_TARGET="aarch64-apple-darwin" - PYO3_CROSS_LIB_DIR="/Library/Frameworks/Python.framework/Versions/$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')/lib/python$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')" + CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl - name: wheels-${{ matrix.os }}-arm + name: wheels-${{ matrix.os }}-32 upload_shared_wheels: name: Upload shared build wheels runs-on: ubuntu-latest environment: release permissions: id-token: write - needs: ["build_wheels", "build_wheels_macos_arm", "build_wheels_32bit"] + needs: ["build_wheels", "build_wheels_32bit", "build_wheels_macos_arm_py38"] steps: - uses: actions/download-artifact@v4 with: diff --git a/pyproject.toml b/pyproject.toml index 975bd377760..97ccde21d1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ target-version = ['py38', 'py39', 'py310', 'py311'] [tool.cibuildwheel] manylinux-x86_64-image = "manylinux2014" manylinux-i686-image = "manylinux2014" -skip = "pp* cp36-* cp37-* *musllinux* *win32 *i686" +skip = "pp* cp36-* cp37-* *musllinux* *win32 *i686 cp38-macosx_arm64" test-skip = "*win32 *linux_i686" test-command = "python {project}/examples/python/stochastic_swap.py" # We need to use pre-built versions of Numpy and Scipy in the tests; they have a diff --git a/releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml b/releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml new file mode 100644 index 00000000000..b59f5b9844c --- /dev/null +++ b/releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml @@ -0,0 +1,11 @@ +--- +other: + - | + Support for the arm64 macOS platform has been promoted from Tier 3 + to Tier 1. Previously the platform was at Tier 3 because there was + no available CI environment for testing Qiskit on the platform. Now + that Github has made an arm64 macOS environment available to open source + projects [#]_ we're testing the platform along with the other Tier 1 + supported platforms. + + .. [#] https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/ diff --git a/tools/build_pgo.sh b/tools/build_pgo.sh index d0e88bf6f74..8553691bdfe 100755 --- a/tools/build_pgo.sh +++ b/tools/build_pgo.sh @@ -17,6 +17,12 @@ else source build_pgo/bin/activate fi +arch=`uname -m` +# Handle macOS calling the architecture arm64 and rust calling it aarch64 +if [[ $arch == "arm64" ]]; then + arch="aarch64" +fi + # Build with instrumentation pip install -U -c constraints.txt setuptools-rust wheel setuptools RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" pip install --prefer-binary -c constraints.txt -r requirements-dev.txt -e . @@ -29,4 +35,4 @@ python tools/pgo_scripts/test_utility_scale.py deactivate -${HOME}/.rustup/toolchains/*x86_64*/lib/rustlib/x86_64*/bin/llvm-profdata merge -o $merged_path /tmp/pgo-data +${HOME}/.rustup/toolchains/*$arch*/lib/rustlib/$arch*/bin/llvm-profdata merge -o $merged_path /tmp/pgo-data From c6ba3a8dbb1ed5c793fea307c56290edbcb0d590 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 1 May 2024 06:04:37 -0400 Subject: [PATCH 012/159] document entanglement="pairwise" for EfficientSU2 (#12314) --- qiskit/circuit/library/n_local/efficient_su2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index fc72a2a6c53..e27fe407e18 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -110,11 +110,11 @@ def __init__( If only one gate is provided, the same gate is applied to each qubit. If a list of gates is provided, all gates are applied to each qubit in the provided order. - entanglement: Specifies the entanglement structure. Can be a string ('full', 'linear' - , 'reverse_linear', 'circular' or 'sca'), a list of integer-pairs specifying the indices - of qubits entangled with one another, or a callable returning such a list provided with - the index of the entanglement layer. - Default to 'reverse_linear' entanglement. + entanglement: Specifies the entanglement structure. Can be a string + ('full', 'linear', 'reverse_linear', 'pairwise', 'circular', or 'sca'), + a list of integer-pairs specifying the indices of qubits entangled with one another, + or a callable returning such a list provided with the index of the entanglement layer. + Defaults to 'reverse_linear' entanglement. Note that 'reverse_linear' entanglement provides the same unitary as 'full' with fewer entangling gates. See the Examples section of :class:`~qiskit.circuit.library.TwoLocal` for more From a4f272f131d1c966c320ce7e8752c3d8ab9aafb0 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 1 May 2024 13:35:36 +0300 Subject: [PATCH 013/159] typos (#12316) --- .../transpiler/passes/synthesis/high_level_synthesis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 3d3e2a6851a..e6442ecfb44 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -732,9 +732,9 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): * use_inverted: Indicates whether to run the algorithm on the inverse matrix and to invert the synthesized circuit. - In certain cases this provides a better decomposition then the direct approach. + In certain cases this provides a better decomposition than the direct approach. * use_transposed: Indicates whether to run the algorithm on the transposed matrix - and to invert the order oF CX gates in the synthesized circuit. + and to invert the order of CX gates in the synthesized circuit. In certain cases this provides a better decomposition than the direct approach. """ @@ -778,9 +778,9 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): * section size: The size of each section used in the Patel–Markov–Hayes algorithm [1]. * use_inverted: Indicates whether to run the algorithm on the inverse matrix and to invert the synthesized circuit. - In certain cases this provides a better decomposition then the direct approach. + In certain cases this provides a better decomposition than the direct approach. * use_transposed: Indicates whether to run the algorithm on the transposed matrix - and to invert the order oF CX gates in the synthesized circuit. + and to invert the order of CX gates in the synthesized circuit. In certain cases this provides a better decomposition than the direct approach. References: From a65c9e628c3932b5984c13acb0a35d0645567901 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Wed, 1 May 2024 13:35:51 +0200 Subject: [PATCH 014/159] Fixing Operator.from_circuit for circuits with final layouts and a non-trivial initial layout (#12057) * reno, format, test changes * fix Operator.from_circuit and add failing test case * Update test_operator.py * Update operator.py * Update releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml Co-authored-by: Matthew Treinish * selective merge of 11399 * reimplementing from_circuit --------- Co-authored-by: Matthew Treinish Co-authored-by: AlexanderIvrii --- qiskit/quantum_info/operators/operator.py | 49 +++++++++++------- ...-from-circuit-bugfix-5dab5993526a2b0a.yaml | 7 +++ .../quantum_info/operators/test_operator.py | 51 +++++++++++++++++-- 3 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index d119a381249..41eac356357 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -82,6 +82,9 @@ def __init__( a Numpy array of shape (2**N, 2**N) qubit systems will be used. If the input operator is not an N-qubit operator, it will assign a single subsystem with dimension specified by the shape of the input. + Note that two operators initialized via this method are only considered equivalent if they + match up to their canonical qubit order (or: permutation). See :meth:`.Operator.from_circuit` + to specify a different qubit permutation. """ op_shape = None if isinstance(data, (list, np.ndarray)): @@ -391,8 +394,7 @@ def from_circuit( Returns: Operator: An operator representing the input circuit """ - dimension = 2**circuit.num_qubits - op = cls(np.eye(dimension)) + if layout is None: if not ignore_set_layout: layout = getattr(circuit, "_layout", None) @@ -403,27 +405,38 @@ def from_circuit( initial_layout=layout, input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)}, ) + + initial_layout = layout.initial_layout if layout is not None else None + if final_layout is None: if not ignore_set_layout and layout is not None: final_layout = getattr(layout, "final_layout", None) - qargs = None - # If there was a layout specified (either from the circuit - # or via user input) use that to set qargs to permute qubits - # based on that layout - if layout is not None: - physical_to_virtual = layout.initial_layout.get_physical_bits() - qargs = [ - layout.input_qubit_mapping[physical_to_virtual[physical_bit]] - for physical_bit in range(len(physical_to_virtual)) - ] - # Convert circuit to an instruction - instruction = circuit.to_instruction() - op._append_instruction(instruction, qargs=qargs) - # If final layout is set permute output indices based on layout + from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern + + if initial_layout is not None: + input_qubits = [None] * len(layout.input_qubit_mapping) + for q, p in layout.input_qubit_mapping.items(): + input_qubits[p] = q + + initial_permutation = initial_layout.to_permutation(input_qubits) + initial_permutation_inverse = _inverse_pattern(initial_permutation) + if final_layout is not None: - perm_pattern = [final_layout._v2p[v] for v in circuit.qubits] - op = op.apply_permutation(perm_pattern, front=False) + final_permutation = final_layout.to_permutation(circuit.qubits) + final_permutation_inverse = _inverse_pattern(final_permutation) + + op = Operator(circuit) + + if initial_layout: + op = op.apply_permutation(initial_permutation, True) + + if final_layout: + op = op.apply_permutation(final_permutation_inverse, False) + + if initial_layout: + op = op.apply_permutation(initial_permutation_inverse, False) + return op def is_unitary(self, atol=None, rtol=None): diff --git a/releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml b/releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml new file mode 100644 index 00000000000..759f023efc8 --- /dev/null +++ b/releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an issue with the :meth:`.Operator.from_circuit` constructor method where it would incorrectly + interpret the final layout permutation resulting in an invalid `Operator` being constructed. + Previously, the final layout was processed without regards for the initial layout, i.e. the + initialization was incorrect for all quantum circuits that have a non-trivial initial layout. diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index fc824643a0b..d653d618201 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -17,6 +17,7 @@ import unittest import logging import copy + from test import combine import numpy as np from ddt import ddt @@ -26,6 +27,7 @@ from qiskit import QiskitError from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit.library import HGate, CHGate, CXGate, QFT +from qiskit.transpiler import CouplingMap from qiskit.transpiler.layout import Layout, TranspileLayout from qiskit.quantum_info.operators import Operator, ScalarOp from qiskit.quantum_info.operators.predicates import matrix_equal @@ -735,6 +737,28 @@ def test_from_circuit_constructor_no_layout(self): global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True) self.assertTrue(global_phase_equivalent) + def test_from_circuit_initial_layout_final_layout(self): + """Test initialization from a circuit with a non-trivial initial_layout and final_layout as given + by a transpiled circuit.""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(2, 1) + qc.cx(1, 2) + qc.cx(1, 0) + qc.cx(1, 3) + qc.cx(1, 4) + qc.h(2) + + qc_transpiled = transpile( + qc, + coupling_map=CouplingMap.from_line(5), + initial_layout=[2, 3, 4, 0, 1], + optimization_level=1, + seed_transpiler=17, + ) + + self.assertTrue(Operator.from_circuit(qc_transpiled).equiv(qc)) + def test_from_circuit_constructor_reverse_embedded_layout(self): """Test initialization from a circuit with an embedded reverse layout.""" # Test tensor product of 1-qubit gates @@ -817,7 +841,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_final_layout(self) circuit._layout = TranspileLayout( Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}), {qubit: index for index, qubit in enumerate(circuit.qubits)}, - Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0}), + Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1}), ) circuit.swap(0, 1) circuit.swap(1, 2) @@ -839,7 +863,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_manual_final_layou Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}), {qubit: index for index, qubit in enumerate(circuit.qubits)}, ) - final_layout = Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0}) + final_layout = Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1}) circuit.swap(0, 1) circuit.swap(1, 2) op = Operator.from_circuit(circuit, final_layout=final_layout) @@ -966,7 +990,7 @@ def test_from_circuit_constructor_empty_layout(self): circuit.h(0) circuit.cx(0, 1) layout = Layout() - with self.assertRaises(IndexError): + with self.assertRaises(KeyError): Operator.from_circuit(circuit, layout=layout) def test_compose_scalar(self): @@ -1078,6 +1102,27 @@ def test_from_circuit_mixed_reg_loose_bits_transpiled(self): result = Operator.from_circuit(tqc) self.assertTrue(Operator(circuit).equiv(result)) + def test_from_circuit_into_larger_map(self): + """Test from_circuit method when the number of physical + qubits is larger than the number of original virtual qubits.""" + + # original circuit on 3 qubits + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + + # transpile into 5-qubits + tqc = transpile(qc, coupling_map=CouplingMap.from_line(5), initial_layout=[0, 2, 4]) + + # qc expanded with ancilla qubits + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.cx(1, 2) + + self.assertEqual(Operator.from_circuit(tqc), Operator(expected)) + def test_apply_permutation_back(self): """Test applying permutation to the operator, where the operator is applied first and the permutation second.""" From cd03721e5f652c79088fb1a0495c296daa05935d Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 1 May 2024 15:56:29 +0300 Subject: [PATCH 015/159] HLSConfig option to run multiple plugins and to choose the best decomposition (#12108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix docstring * import * exposing additional plugin arguments * tests * lint * release notes * HLS config option to run all specified plugins + tests * lint" * removing todo * release notes * fixes --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../passes/synthesis/high_level_synthesis.py | 49 ++++++++++--- ...n-all-plugins-option-ba8806a269e5713c.yaml | 51 +++++++++++++ .../transpiler/test_high_level_synthesis.py | 73 +++++++++++++++++++ 3 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index e6442ecfb44..fd21ae6a75f 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -133,7 +133,7 @@ TokenSwapperSynthesisPermutation """ -from typing import Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple, Callable import numpy as np import rustworkx as rx @@ -227,16 +227,34 @@ class HLSConfig: :ref:`using-high-level-synthesis-plugins`. """ - def __init__(self, use_default_on_unspecified=True, **kwargs): + def __init__( + self, + use_default_on_unspecified: bool = True, + plugin_selection: str = "sequential", + plugin_evaluation_fn: Optional[Callable[[QuantumCircuit], int]] = None, + **kwargs, + ): """Creates a high-level-synthesis config. Args: - use_default_on_unspecified (bool): if True, every higher-level-object without an + use_default_on_unspecified: if True, every higher-level-object without an explicitly specified list of methods will be synthesized using the "default" algorithm if it exists. + plugin_selection: if set to ``"sequential"`` (default), for every higher-level-object + the synthesis pass will consider the specified methods sequentially, stopping + at the first method that is able to synthesize the object. If set to ``"all"``, + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. + plugin_evaluation_fn: a callable that evaluates the quality of the synthesized + quantum circuit; a smaller value means a better circuit. If ``None``, the + quality of the circuit its size (i.e. the number of gates that it contains). kwargs: a dictionary mapping higher-level-objects to lists of synthesis methods. """ self.use_default_on_unspecified = use_default_on_unspecified + self.plugin_selection = plugin_selection + self.plugin_evaluation_fn = ( + plugin_evaluation_fn if plugin_evaluation_fn is not None else lambda qc: qc.size() + ) self.methods = {} for key, value in kwargs.items(): @@ -248,9 +266,6 @@ def set_methods(self, hls_name, hls_methods): self.methods[hls_name] = hls_methods -# ToDo: Do we have a way to specify optimization criteria (e.g., 2q gate count vs. depth)? - - class HighLevelSynthesis(TransformationPass): """Synthesize higher-level objects and unroll custom definitions. @@ -500,6 +515,9 @@ def _synthesize_op_using_plugins( else: methods = [] + best_decomposition = None + best_score = np.inf + for method in methods: # 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 @@ -538,11 +556,22 @@ def _synthesize_op_using_plugins( ) # The synthesis methods that are not suited for the given higher-level-object - # will return None, in which case the next method in the list will be used. + # will return None. if decomposition is not None: - return decomposition - - return None + if self.hls_config.plugin_selection == "sequential": + # In the "sequential" mode the first successful decomposition is + # returned. + best_decomposition = decomposition + break + + # In the "run everything" mode we update the best decomposition + # discovered + current_score = self.hls_config.plugin_evaluation_fn(decomposition) + if current_score < best_score: + best_decomposition = decomposition + best_score = current_score + + return best_decomposition def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: """ diff --git a/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml b/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml new file mode 100644 index 00000000000..2ab34c61fb3 --- /dev/null +++ b/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml @@ -0,0 +1,51 @@ +--- +features: + - | + The :class:`~.HLSConfig` now has two additional optional arguments. The argument + ``plugin_selection`` can be set either to ``"sequential"`` or to ``"all"``. + If set to "sequential" (default), for every higher-level-object + the :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass will consider the + specified methods sequentially, in the order they appear in the list, stopping + at the first method that is able to synthesize the object. If set to "all", + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. The argument + ``plugin_evaluation_fn`` is an optional callable that evaluates the quality of + the synthesized quantum circuit; a smaller value means a better circuit. When + set to ``None``, the quality of the circuit is its size (i.e. the number of gates + that it contains). + + The following example illustrates the new functionality:: + + from qiskit import QuantumCircuit + from qiskit.circuit.library import LinearFunction + from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.transpiler.passes import HighLevelSynthesis, HLSConfig + + # Create a circuit with a linear function + mat = random_invertible_binary_matrix(7, seed=37) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + # Run different methods with different parameters, + # choosing the best result in terms of depth. + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ("pmh", {"section_size": 1}), + ("pmh", {"section_size": 3}), + ("kms", {}), + ("kms", {"use_inverted": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda circuit: circuit.depth(), + ) + + # synthesize + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + + In the example, we run multiple synthesis methods with different parameters, + choosing the best circuit in terms of depth. Note that optimizing + ``circuit.size()`` instead would pick a different circuit. diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 5ab78af8f58..0f074865f41 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -65,6 +65,7 @@ ) from test import QiskitTestCase # pylint: disable=wrong-import-order + # In what follows, we create two simple operations OpA and OpB, that potentially mimic # higher-level objects written by a user. # For OpA we define two synthesis methods: @@ -586,6 +587,78 @@ def test_invert_and_transpose(self): self.assertEqual(qct.size(), 6) self.assertEqual(qct.depth(), 6) + def test_plugin_selection_all(self): + """Test setting plugin_selection to all.""" + + linear_function = LinearFunction(self.construct_linear_circuit(7)) + qc = QuantumCircuit(7) + qc.append(linear_function, [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("sequential"): + # In the default "run sequential" mode, we stop as soon as a plugin + # in the list returns a circuit. + # For this specific example the default options lead to a suboptimal circuit. + hls_config = HLSConfig(linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})]) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 12) + self.assertEqual(qct.depth(), 8) + + with self.subTest("all"): + # In the non-default "run all" mode, we examine all plugins in the list. + # For this specific example we get the better result for the second plugin in the list. + hls_config = HLSConfig( + linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})], + plugin_selection="all", + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 6) + self.assertEqual(qct.depth(), 6) + + def test_plugin_selection_all_with_metrix(self): + """Test setting plugin_selection to all and specifying different evaluation functions.""" + + # The seed is chosen so that we get different best circuits depending on whether we + # want to minimize size or depth. + mat = random_invertible_binary_matrix(7, seed=37) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("size_fn"): + # We want to minimize the "size" (aka the number of gates) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.size(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 20) + self.assertEqual(qct.depth(), 15) + + with self.subTest("depth_fn"): + # We want to minimize the "depth" (aka the number of layers) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.depth(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 23) + self.assertEqual(qct.depth(), 12) + class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the KMSSynthesisLinearFunction plugin for synthesizing linear functions.""" From c60a1ed63eef38be2590bbdd9c93709ac7eb94c8 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 1 May 2024 13:59:43 +0100 Subject: [PATCH 016/159] Add representation of `expr.Var` to `DAGCircuit` (#12204) * Add representation of `expr.Var` to `DAGCircuit` This adds basic support for `Var`s to `DAGCircuit`, enabling the base representation using the same wire structure used for clbits. This is known to be an inefficient representation of the dataflow for clbits, which can be read multiple times without implying an order between those operations (unlike qubits for which reads and writes are more naturally linked). We're using this simpler representation to make a better initial MVP; optimising the data-flow representation would come as part of a larger effort within the `DAGCircuit`. This commit adds support in `DAGCircuit` for: - representation of `Var` nodes - appending all operations that might contain `Var` nodes to the DAG and updating the wire structure (including control-flow ops and stores) - equality checking of DAGs with `Var`s (and so enables `QuantumCircuit.__eq__` as well) - `DAGCircuit.copy_empty_like` with `Var`s - the DAG/circuit converters The other methods in `DAGCircuit` that might need to be aware of `Var` nodes will be handled separately. * Expand test coverage * Fix copy/paste error --- qiskit/circuit/controlflow/control_flow.py | 10 + qiskit/converters/circuit_to_dag.py | 7 + qiskit/converters/dag_to_circuit.py | 4 + qiskit/dagcircuit/dagcircuit.py | 309 ++++++++++++++---- test/python/converters/test_circuit_to_dag.py | 36 +- test/python/dagcircuit/test_dagcircuit.py | 245 +++++++++++++- 6 files changed, 538 insertions(+), 73 deletions(-) diff --git a/qiskit/circuit/controlflow/control_flow.py b/qiskit/circuit/controlflow/control_flow.py index 51b3709db6b..2085f760ebc 100644 --- a/qiskit/circuit/controlflow/control_flow.py +++ b/qiskit/circuit/controlflow/control_flow.py @@ -22,6 +22,7 @@ if typing.TYPE_CHECKING: from qiskit.circuit import QuantumCircuit + from qiskit.circuit.classical import expr class ControlFlowOp(Instruction, ABC): @@ -72,3 +73,12 @@ def map_block(block: QuantumCircuit) -> QuantumCircuit: Returns: New :class:`ControlFlowOp` with replaced blocks. """ + + def iter_captured_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterator over the unique captured variables in all blocks of this construct.""" + seen = set() + for block in self.blocks: + for var in block.iter_captured_vars(): + if var not in seen: + seen.add(var) + yield var diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index e2612b43d3e..b2c1df2a037 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -79,6 +79,13 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord dagcircuit.add_qubits(qubits) dagcircuit.add_clbits(clbits) + for var in circuit.iter_input_vars(): + dagcircuit.add_input_var(var) + for var in circuit.iter_captured_vars(): + dagcircuit.add_captured_var(var) + for var in circuit.iter_declared_vars(): + dagcircuit.add_declared_var(var) + for register in circuit.qregs: dagcircuit.add_qreg(register) diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 5a32f0bba1e..ede026c247c 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -62,7 +62,11 @@ def dag_to_circuit(dag, copy_operations=True): *dag.cregs.values(), name=name, global_phase=dag.global_phase, + inputs=dag.iter_input_vars(), + captures=dag.iter_captured_vars(), ) + for var in dag.iter_declared_vars(): + circuit.add_uninitialized_var(var) circuit.metadata = dag.metadata circuit.calibrations = dag.calibrations diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 8c1332a8e60..6f00a3b3ea7 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -22,10 +22,12 @@ """ from __future__ import annotations -from collections import OrderedDict, defaultdict, deque, namedtuple -from collections.abc import Callable, Sequence, Generator, Iterable import copy +import enum +import itertools import math +from collections import OrderedDict, defaultdict, deque, namedtuple +from collections.abc import Callable, Sequence, Generator, Iterable from typing import Any import numpy as np @@ -39,7 +41,9 @@ SwitchCaseOp, _classical_resource_map, Operation, + Store, ) +from qiskit.circuit.classical import expr from qiskit.circuit.controlflow import condition_resources, node_resources, CONTROL_FLOW_OP_NAMES from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit @@ -78,13 +82,24 @@ def __init__(self): # Cache of dag op node sort keys self._key_cache = {} - # Set of wires (Register,idx) in the dag + # Set of wire data in the DAG. A wire is an owned unit of data. Qubits are the primary + # wire type (and the only data that has _true_ wire properties from a read/write + # perspective), but clbits and classical `Var`s are too. Note: classical registers are + # _not_ wires because the individual bits are the more fundamental unit. We treat `Var`s + # as the entire wire (as opposed to individual bits of them) for scalability reasons; if a + # parametric program wants to parametrize over 16-bit angles, we can't scale to 1000s of + # those by tracking all 16 bits individually. + # + # Classical variables shouldn't be "wires"; it should be possible to have multiple reads + # without implying ordering. The initial addition of the classical variables uses the + # existing wire structure as an MVP; we expect to handle this better in a new version of the + # transpiler IR that also handles control flow more properly. self._wires = set() - # Map from wire (Register,idx) to input nodes of the graph + # Map from wire to input nodes of the graph self.input_map = OrderedDict() - # Map from wire (Register,idx) to output nodes of the graph + # Map from wire to output nodes of the graph self.output_map = OrderedDict() # Directed multigraph whose nodes are inputs, outputs, or operations. @@ -92,7 +107,7 @@ def __init__(self): # additional data about the operation, including the argument order # and parameter values. # Input nodes have out-degree 1 and output nodes have in-degree 1. - # Edges carry wire labels (reg,idx) and each operation has + # Edges carry wire labels and each operation has # corresponding in- and out-edges with the same wire labels. self._multi_graph = rx.PyDAG() @@ -110,6 +125,16 @@ def __init__(self): # its index within that register. self._qubit_indices: dict[Qubit, BitLocations] = {} self._clbit_indices: dict[Clbit, BitLocations] = {} + # Tracking for the classical variables used in the circuit. This contains the information + # needed to insert new nodes. This is keyed by the name rather than the `Var` instance + # itself so we can ensure we don't allow shadowing or redefinition of names. + self._vars_info: dict[str, _DAGVarInfo] = {} + # Convenience stateful tracking for the individual types of nodes to allow things like + # comparisons between circuits to take place without needing to disambiguate the + # graph-specific usage information. + self._vars_by_type: dict[_DAGVarType, set[expr.Var]] = { + type_: set() for type_ in _DAGVarType + } self._global_phase: float | ParameterExpression = 0.0 self._calibrations: dict[str, dict[tuple, Schedule]] = defaultdict(dict) @@ -122,7 +147,11 @@ def __init__(self): @property def wires(self): """Return a list of the wires in order.""" - return self.qubits + self.clbits + return ( + self.qubits + + self.clbits + + [var for vars in self._vars_by_type.values() for var in vars] + ) @property def node_counter(self): @@ -297,6 +326,57 @@ def add_creg(self, creg): ) self._add_wire(creg[j]) + def add_input_var(self, var: expr.Var): + """Add an input variable to the circuit. + + Args: + var: the variable to add.""" + if self._vars_by_type[_DAGVarType.CAPTURE]: + raise DAGCircuitError("cannot add inputs to a circuit with captures") + self._add_var(var, _DAGVarType.INPUT) + + def add_captured_var(self, var: expr.Var): + """Add a captured variable to the circuit. + + Args: + var: the variable to add.""" + if self._vars_by_type[_DAGVarType.INPUT]: + raise DAGCircuitError("cannot add captures to a circuit with inputs") + self._add_var(var, _DAGVarType.CAPTURE) + + def add_declared_var(self, var: expr.Var): + """Add a declared local variable to the circuit. + + Args: + var: the variable to add.""" + self._add_var(var, _DAGVarType.DECLARE) + + def _add_var(self, var: expr.Var, type_: _DAGVarType): + """Inner function to add any variable to the DAG. ``location`` should be a reference one of + the ``self._vars_*`` tracking dictionaries. + """ + # The setup of the initial graph structure between an "in" and an "out" node is the same as + # the bit-related `_add_wire`, but this logically needs to do different bookkeeping around + # tracking the properties. + if not var.standalone: + raise DAGCircuitError( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances" + ) + if (previous := self._vars_info.get(var.name, None)) is not None: + if previous.var == var: + raise DAGCircuitError(f"'{var}' is already present in the circuit") + raise DAGCircuitError( + f"cannot add '{var}' as its name shadows the existing '{previous.var}'" + ) + in_node = DAGInNode(wire=var) + out_node = DAGOutNode(wire=var) + in_node._node_id, out_node._node_id = self._multi_graph.add_nodes_from((in_node, out_node)) + self._multi_graph.add_edge(in_node._node_id, out_node._node_id, var) + self.input_map[var] = in_node + self.output_map[var] = out_node + self._vars_by_type[type_].add(var) + self._vars_info[var.name] = _DAGVarInfo(var, type_, in_node, out_node) + def _add_wire(self, wire): """Add a qubit or bit to the circuit. @@ -543,14 +623,14 @@ def _check_condition(self, name, condition): if not set(resources.clbits).issubset(self.clbits): raise DAGCircuitError(f"invalid clbits in condition for {name}") - def _check_bits(self, args, amap): - """Check the values of a list of (qu)bit arguments. + def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var, Any]): + """Check the values of a list of wire arguments. For each element of args, check that amap contains it. Args: - args (list[Bit]): the elements to be checked - amap (dict): a dictionary keyed on Qubits/Clbits + args: the elements to be checked + amap: a dictionary keyed on Qubits/Clbits Raises: DAGCircuitError: if a qubit is not contained in amap @@ -558,46 +638,7 @@ def _check_bits(self, args, amap): # Check for each wire for wire in args: if wire not in amap: - raise DAGCircuitError(f"(qu)bit {wire} not found in {amap}") - - @staticmethod - def _bits_in_operation(operation): - """Return an iterable over the classical bits that are inherent to an instruction. This - includes a `condition`, or the `target` of a :class:`.ControlFlowOp`. - - Args: - instruction: the :class:`~.circuit.Instruction` instance for a node. - - Returns: - Iterable[Clbit]: the :class:`.Clbit`\\ s involved. - """ - # If updating this, also update the fast-path checker `DAGCirucit._operation_may_have_bits`. - if (condition := getattr(operation, "condition", None)) is not None: - yield from condition_resources(condition).clbits - if isinstance(operation, SwitchCaseOp): - target = operation.target - if isinstance(target, Clbit): - yield target - elif isinstance(target, ClassicalRegister): - yield from target - else: - yield from node_resources(target).clbits - - @staticmethod - def _operation_may_have_bits(operation) -> bool: - """Return whether a given :class:`.Operation` may contain any :class:`.Clbit` instances - in itself (e.g. a control-flow operation). - - Args: - operation (qiskit.circuit.Operation): the operation to check. - """ - # This is separate to `_bits_in_operation` because most of the time there won't be any bits, - # so we want a fast path to be able to skip creating and testing a generator for emptiness. - # - # If updating this, also update `DAGCirucit._bits_in_operation`. - return getattr(operation, "condition", None) is not None or isinstance( - operation, SwitchCaseOp - ) + raise DAGCircuitError(f"wire {wire} not found in {amap}") def _increment_op(self, op): if op.name in self._op_names: @@ -618,7 +659,8 @@ def copy_empty_like(self): * name and other metadata * global phase * duration - * all the qubits and clbits, including the registers. + * all the qubits and clbits, including the registers + * all the classical variables. Returns: DAGCircuit: An empty copy of self. @@ -639,6 +681,13 @@ def copy_empty_like(self): for creg in self.cregs.values(): target_dag.add_creg(creg) + for var in self.iter_input_vars(): + target_dag.add_input_var(var) + for var in self.iter_captured_vars(): + target_dag.add_captured_var(var) + for var in self.iter_declared_vars(): + target_dag.add_declared_var(var) + return target_dag def apply_operation_back( @@ -669,17 +718,17 @@ def apply_operation_back( """ qargs = tuple(qargs) cargs = tuple(cargs) + additional = () - if self._operation_may_have_bits(op): + if _may_have_additional_wires(op): # This is the slow path; most of the time, this won't happen. - all_cbits = set(self._bits_in_operation(op)).union(cargs) - else: - all_cbits = cargs + additional = set(_additional_wires(op)).difference(cargs) if check: self._check_condition(op.name, getattr(op, "condition", None)) - self._check_bits(qargs, self.output_map) - self._check_bits(all_cbits, self.output_map) + self._check_wires(qargs, self.output_map) + self._check_wires(cargs, self.output_map) + self._check_wires(additional, self.output_map) node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) @@ -690,7 +739,7 @@ def apply_operation_back( # and adding new edges from the operation node to each output node self._multi_graph.insert_node_on_in_edges_multiple( node._node_id, - [self.output_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits], + [self.output_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], ) return node @@ -721,17 +770,17 @@ def apply_operation_front( """ qargs = tuple(qargs) cargs = tuple(cargs) + additional = () - if self._operation_may_have_bits(op): + if _may_have_additional_wires(op): # This is the slow path; most of the time, this won't happen. - all_cbits = set(self._bits_in_operation(op)).union(cargs) - else: - all_cbits = cargs + additional = set(_additional_wires(op)).difference(cargs) if check: self._check_condition(op.name, getattr(op, "condition", None)) - self._check_bits(qargs, self.input_map) - self._check_bits(all_cbits, self.input_map) + self._check_wires(qargs, self.output_map) + self._check_wires(cargs, self.output_map) + self._check_wires(additional, self.output_map) node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) @@ -742,7 +791,7 @@ def apply_operation_front( # and adding new edges to the operation node from each input node self._multi_graph.insert_node_on_out_edges_multiple( node._node_id, - [self.input_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits], + [self.input_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], ) return node @@ -1030,6 +1079,42 @@ def num_tensor_factors(self): """Compute how many components the circuit can decompose into.""" return rx.number_weakly_connected_components(self._multi_graph) + @property + def num_vars(self): + """Total number of classical variables tracked by the circuit.""" + return len(self._vars_info) + + @property + def num_input_vars(self): + """Number of input classical variables tracked by the circuit.""" + return len(self._vars_by_type[_DAGVarType.INPUT]) + + @property + def num_captured_vars(self): + """Number of captured classical variables tracked by the circuit.""" + return len(self._vars_by_type[_DAGVarType.CAPTURE]) + + @property + def num_declared_vars(self): + """Number of declared local classical variables tracked by the circuit.""" + return len(self._vars_by_type[_DAGVarType.DECLARE]) + + def iter_vars(self): + """Iterable over all the classical variables tracked by the circuit.""" + return itertools.chain.from_iterable(self._vars_by_type.values()) + + def iter_input_vars(self): + """Iterable over the input classical variables tracked by the circuit.""" + return iter(self._vars_by_type[_DAGVarType.INPUT]) + + def iter_captured_vars(self): + """Iterable over the captured classical variables tracked by the circuit.""" + return iter(self._vars_by_type[_DAGVarType.CAPTURE]) + + def iter_declared_vars(self): + """Iterable over the declared local classical variables tracked by the circuit.""" + return iter(self._vars_by_type[_DAGVarType.DECLARE]) + def __eq__(self, other): # Try to convert to float, but in case of unbound ParameterExpressions # a TypeError will be raise, fallback to normal equality in those @@ -1047,6 +1132,11 @@ def __eq__(self, other): if self.calibrations != other.calibrations: return False + # We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be + # equal in our mind if they use the exact same UUID vars. + if self._vars_by_type != other._vars_by_type: + return False + self_bit_indices = {bit: idx for idx, bit in enumerate(self.qubits + self.clbits)} other_bit_indices = {bit: idx for idx, bit in enumerate(other.qubits + other.clbits)} @@ -1227,9 +1317,9 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit node_wire_order = list(node.qargs) + list(node.cargs) # If we're not propagating it, the number of wires in the input DAG should include the # condition as well. - if not propagate_condition and self._operation_may_have_bits(node.op): + if not propagate_condition and _may_have_additional_wires(node.op): node_wire_order += [ - bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs + wire for wire in _additional_wires(node.op) if wire not in node_cargs ] if len(wires) != len(node_wire_order): raise DAGCircuitError( @@ -1706,7 +1796,7 @@ def classical_predecessors(self, node): connected by a classical edge as DAGOpNodes and DAGInNodes.""" return iter( self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) ) ) @@ -1739,7 +1829,7 @@ def classical_successors(self, node): connected by a classical edge as DAGOpNodes and DAGInNodes.""" return iter( self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) ) ) @@ -2123,3 +2213,82 @@ def draw(self, scale=0.7, filename=None, style="color"): from qiskit.visualization.dag_visualization import dag_drawer return dag_drawer(dag=self, scale=scale, filename=filename, style=style) + + +class _DAGVarType(enum.Enum): + INPUT = enum.auto() + CAPTURE = enum.auto() + DECLARE = enum.auto() + + +class _DAGVarInfo: + __slots__ = ("var", "type", "in_node", "out_node") + + def __init__(self, var: expr.Var, type_: _DAGVarType, in_node: DAGInNode, out_node: DAGOutNode): + self.var = var + self.type = type_ + self.in_node = in_node + self.out_node = out_node + + +def _may_have_additional_wires(operation) -> bool: + """Return whether a given :class:`.Operation` may contain references to additional wires + locations within itself. If this is ``False``, it doesn't necessarily mean that the operation + _will_ access memory inherently, but a ``True`` return guarantees that it won't. + + The memory might be classical bits or classical variables, such as a control-flow operation or a + store. + + Args: + operation (qiskit.circuit.Operation): the operation to check. + """ + # This is separate to `_additional_wires` because most of the time there won't be any extra + # wires beyond the explicit `qargs` and `cargs` so we want a fast path to be able to skip + # creating and testing a generator for emptiness. + # + # If updating this, you most likely also need to update `_additional_wires`. + return getattr(operation, "condition", None) is not None or isinstance( + operation, (ControlFlowOp, Store) + ) + + +def _additional_wires(operation) -> Iterable[Clbit | expr.Var]: + """Return an iterable over the additional tracked memory usage in this operation. These + additional wires include (for example, non-exhaustive) bits referred to by a ``condition`` or + the classical variables involved in control-flow operations. + + Args: + operation: the :class:`~.circuit.Operation` instance for a node. + + Returns: + Iterable: the additional wires inherent to this operation. + """ + # If updating this, you likely need to update `_may_have_additional_wires` too. + if (condition := getattr(operation, "condition", None)) is not None: + if isinstance(condition, expr.Expr): + yield from _wires_from_expr(condition) + else: + yield from condition_resources(condition).clbits + if isinstance(operation, ControlFlowOp): + yield from operation.iter_captured_vars() + if isinstance(operation, SwitchCaseOp): + target = operation.target + if isinstance(target, Clbit): + yield target + elif isinstance(target, ClassicalRegister): + yield from target + else: + yield from _wires_from_expr(target) + elif isinstance(operation, Store): + yield from _wires_from_expr(operation.lvalue) + yield from _wires_from_expr(operation.rvalue) + + +def _wires_from_expr(node: expr.Expr) -> Iterable[Clbit | expr.Var]: + for var in expr.iter_vars(node): + if isinstance(var.var, Clbit): + yield var.var + elif isinstance(var.var, ClassicalRegister): + yield from var.var + else: + yield var diff --git a/test/python/converters/test_circuit_to_dag.py b/test/python/converters/test_circuit_to_dag.py index 4f2f52d0378..0bded9c0f4a 100644 --- a/test/python/converters/test_circuit_to_dag.py +++ b/test/python/converters/test_circuit_to_dag.py @@ -15,9 +15,9 @@ import unittest from qiskit.dagcircuit import DAGCircuit -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Clbit +from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Clbit, SwitchCaseOp from qiskit.circuit.library import HGate, Measure -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.converters import dag_to_circuit, circuit_to_dag from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -106,6 +106,38 @@ def test_wires_from_expr_nodes_target(self): for original, test in zip(outer, roundtripped): self.assertEqual(original.operation.target, test.operation.target) + def test_runtime_vars_in_roundtrip(self): + """`expr.Var` nodes should be fully roundtripped.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + d = expr.Var.new("d", types.Uint(8)) + qc = QuantumCircuit(inputs=[a, c]) + qc.add_var(b, False) + qc.add_var(d, 255) + qc.store(a, expr.logic_or(a, b)) + with qc.if_test(expr.logic_and(a, expr.equal(c, d))): + pass + with qc.while_loop(a): + qc.store(a, expr.logic_or(a, b)) + with qc.switch(d) as case: + with case(0): + qc.store(c, d) + with case(case.DEFAULT): + qc.store(a, False) + + roundtrip = dag_to_circuit(circuit_to_dag(qc)) + self.assertEqual(qc, roundtrip) + + self.assertIsInstance(qc.data[-1].operation, SwitchCaseOp) + # This is guaranteed to be topologically last, even after the DAG roundtrip. + self.assertIsInstance(roundtrip.data[-1].operation, SwitchCaseOp) + self.assertEqual(qc.data[-1].operation.blocks, roundtrip.data[-1].operation.blocks) + + blocks = roundtrip.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_captured_vars()), {c, d}) + self.assertEqual(set(blocks[1].iter_captured_vars()), {a}) + def test_wire_order(self): """Test that the `qubit_order` and `clbit_order` parameters are respected.""" permutation = [2, 3, 1, 4, 0, 5] # Arbitrary. diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 3fcf5ff7a27..62172d084ad 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -38,8 +38,10 @@ SwitchCaseOp, IfElseOp, WhileLoopOp, + CASE_DEFAULT, + Store, ) -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import IGate, HGate, CXGate, CZGate, XGate, YGate, U1Gate, RXGate from qiskit.converters import circuit_to_dag from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -421,6 +423,22 @@ def test_copy_empty_like(self): self.assertEqual(self.dag.duration, result_dag.duration) self.assertEqual(self.dag.unit, result_dag.unit) + def test_copy_empty_like_vars(self): + """Variables should be part of the empty copy.""" + dag = DAGCircuit() + dag.add_input_var(expr.Var.new("a", types.Bool())) + dag.add_input_var(expr.Var.new("b", types.Uint(8))) + dag.add_declared_var(expr.Var.new("c", types.Bool())) + dag.add_declared_var(expr.Var.new("d", types.Uint(8))) + self.assertEqual(dag, dag.copy_empty_like()) + + dag = DAGCircuit() + dag.add_captured_var(expr.Var.new("a", types.Bool())) + dag.add_captured_var(expr.Var.new("b", types.Uint(8))) + dag.add_declared_var(expr.Var.new("c", types.Bool())) + dag.add_declared_var(expr.Var.new("d", types.Uint(8))) + self.assertEqual(dag, dag.copy_empty_like()) + def test_remove_busy_clbit(self): """Classical bit removal of busy classical bits raises.""" self.dag.apply_operation_back(Measure(), [self.qreg[0]], [self.individual_clbit]) @@ -1822,6 +1840,231 @@ def test_semantic_expr(self): qc2.switch(expr.bit_and(cr, 5), [(1, body)], [0], []) self.assertNotEqual(circuit_to_dag(qc1), circuit_to_dag(qc2)) + def test_present_vars(self): + """The vars should be compared whether or not they're used.""" + a_bool = expr.Var.new("a", types.Bool()) + a_u8 = expr.Var.new("a", types.Uint(8)) + a_u8_other = expr.Var.new("a", types.Uint(8)) + b_bool = expr.Var.new("b", types.Bool()) + + left = DAGCircuit() + left.add_input_var(a_bool) + left.add_input_var(b_bool) + self.assertEqual(left.num_input_vars, 2) + self.assertEqual(left.num_captured_vars, 0) + self.assertEqual(left.num_declared_vars, 0) + self.assertEqual(left.num_vars, 2) + + right = DAGCircuit() + right.add_input_var(a_bool) + right.add_input_var(b_bool) + self.assertEqual(right.num_input_vars, 2) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(left.num_vars, 2) + self.assertEqual(left, right) + + right = DAGCircuit() + right.add_input_var(a_u8) + right.add_input_var(b_bool) + self.assertEqual(right.num_input_vars, 2) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + right = DAGCircuit() + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 0) + self.assertNotEqual(left, right) + + right = DAGCircuit() + right.add_captured_var(a_bool) + right.add_captured_var(b_bool) + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 2) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + right = DAGCircuit() + right.add_declared_var(a_bool) + right.add_declared_var(b_bool) + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 2) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + left = DAGCircuit() + left.add_captured_var(a_u8) + + right = DAGCircuit() + right.add_captured_var(a_u8) + self.assertEqual(left, right) + + right = DAGCircuit() + right.add_captured_var(a_u8_other) + self.assertNotEqual(left, right) + + def test_wires_added_for_simple_classical_vars(self): + """Var uses should be represented in the wire structure.""" + a = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_input_var(a) + self.assertEqual(list(dag.iter_vars()), [a]) + self.assertEqual(list(dag.iter_input_vars()), [a]) + self.assertEqual(list(dag.iter_captured_vars()), []) + self.assertEqual(list(dag.iter_declared_vars()), []) + + expected_nodes = [dag.input_map[a], dag.output_map[a]] + self.assertEqual(list(dag.topological_nodes()), expected_nodes) + self.assertTrue(dag.is_successor(dag.input_map[a], dag.output_map[a])) + + op_mid = dag.apply_operation_back(Store(a, expr.lift(True)), (), ()) + self.assertTrue(dag.is_successor(dag.input_map[a], op_mid)) + self.assertTrue(dag.is_successor(op_mid, dag.output_map[a])) + self.assertFalse(dag.is_successor(dag.input_map[a], dag.output_map[a])) + + op_front = dag.apply_operation_front(Store(a, expr.logic_not(a)), (), ()) + self.assertTrue(dag.is_successor(dag.input_map[a], op_front)) + self.assertTrue(dag.is_successor(op_front, op_mid)) + self.assertFalse(dag.is_successor(dag.input_map[a], op_mid)) + + op_back = dag.apply_operation_back(Store(a, expr.logic_not(a)), (), ()) + self.assertTrue(dag.is_successor(op_mid, op_back)) + self.assertTrue(dag.is_successor(op_back, dag.output_map[a])) + self.assertFalse(dag.is_successor(op_mid, dag.output_map[a])) + + def test_wires_added_for_var_control_flow_condition(self): + """Vars used in if/else or while conditionals should be added to the wire structure.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + dag = DAGCircuit() + dag.add_declared_var(a) + dag.add_input_var(b) + + op_store = dag.apply_operation_back(Store(a, expr.lift(False)), (), ()) + op_if = dag.apply_operation_back(IfElseOp(a, QuantumCircuit()), (), ()) + op_while = dag.apply_operation_back( + WhileLoopOp(expr.logic_or(a, b), QuantumCircuit()), (), () + ) + + expected_edges = { + (dag.input_map[a], op_store, a), + (op_store, op_if, a), + (op_if, op_while, a), + (op_while, dag.output_map[a], a), + (dag.input_map[b], op_while, b), + (op_while, dag.output_map[b], b), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_wires_added_for_var_control_flow_target(self): + """Vars used in switch targets should be added to the wire structure.""" + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(8)) + dag = DAGCircuit() + dag.add_declared_var(a) + dag.add_input_var(b) + + op_store = dag.apply_operation_back(Store(a, expr.lift(3, a.type)), (), ()) + op_switch = dag.apply_operation_back( + SwitchCaseOp(expr.bit_xor(a, b), [(CASE_DEFAULT, QuantumCircuit())]), (), () + ) + + expected_edges = { + (dag.input_map[a], op_store, a), + (op_store, op_switch, a), + (op_switch, dag.output_map[a], a), + (dag.input_map[b], op_switch, b), + (op_switch, dag.output_map[b], b), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_wires_added_for_control_flow_captures(self): + """Vars captured in control-flow blocks should be in the wire structure.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_input_var(d) + op_store = dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + op_if = dag.apply_operation_back(IfElseOp(a, QuantumCircuit(captures=[b])), (), ()) + op_switch = dag.apply_operation_back( + SwitchCaseOp( + d, + [ + (0, QuantumCircuit(captures=[b])), + (CASE_DEFAULT, QuantumCircuit(captures=[c])), + ], + ), + (), + (), + ) + + expected_edges = { + # a + (dag.input_map[a], op_if, a), + (op_if, dag.output_map[a], a), + # b + (dag.input_map[b], op_if, b), + (op_if, op_switch, b), + (op_switch, dag.output_map[b], b), + # c + (dag.input_map[c], op_store, c), + (op_store, op_switch, c), + (op_switch, dag.output_map[c], c), + # d + (dag.input_map[d], op_switch, d), + (op_switch, dag.output_map[d], d), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_forbid_mixing_captures_inputs(self): + """Test that a DAG can't have both captures and inputs.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + with self.assertRaisesRegex( + DAGCircuitError, "cannot add captures to a circuit with inputs" + ): + dag.add_captured_var(b) + + dag = DAGCircuit() + dag.add_captured_var(a) + with self.assertRaisesRegex( + DAGCircuitError, "cannot add inputs to a circuit with captures" + ): + dag.add_input_var(b) + + def test_forbid_adding_nonstandalone_var(self): + """Temporary "wrapping" vars aren't standalone and can't be tracked separately.""" + dag = DAGCircuit() + with self.assertRaisesRegex(DAGCircuitError, "cannot add variables that wrap"): + dag.add_input_var(expr.lift(ClassicalRegister(4, "c"))) + with self.assertRaisesRegex(DAGCircuitError, "cannot add variables that wrap"): + dag.add_declared_var(expr.lift(Clbit())) + + def test_forbid_adding_conflicting_vars(self): + """Can't re-add a variable that exists, nor a shadowing variable in the same scope.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_declared_var(a1) + with self.assertRaisesRegex(DAGCircuitError, "already present in the circuit"): + dag.add_declared_var(a1) + with self.assertRaisesRegex(DAGCircuitError, "cannot add .* as its name shadows"): + dag.add_declared_var(a2) + class TestDagSubstitute(QiskitTestCase): """Test substituting a dag node with a sub-dag""" From b40a34e9f24410855ffbcbacc857de10af57f08d Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 1 May 2024 09:10:09 -0400 Subject: [PATCH 017/159] fix UnitaryOverlap docstring tex (#12306) --- qiskit/circuit/library/overlap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index ed86d8abb9a..38f5fb9184e 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -26,11 +26,11 @@ class UnitaryOverlap(QuantumCircuit): names `"p1"` (for circuit ``unitary1``) and `"p2"` (for circuit ``unitary_2``) in the output circuit. - This circuit is usually employed in computing the fidelity:: + This circuit is usually employed in computing the fidelity: - .. math:: + .. math:: - \left|\langle 0| U_2^{\dag} U_1|0\rangle\right|^{2} + \left|\langle 0| U_2^{\dag} U_1|0\rangle\right|^{2} by computing the probability of being in the all-zeros bit-string, or equivalently, the expectation value of projector :math:`|0\rangle\langle 0|`. From a41690d6075ac9438ff01b2346a35aa21dc3a568 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 1 May 2024 16:41:29 +0100 Subject: [PATCH 018/159] Reject vars in `QuantumCircuit.to_instruction` and `to_gate` (#12207) * Reject vars in `QuantumCircuit.to_instruction` and `to_gate` `QuantumCircuit.to_gate` may be able to support `input` variables that are angle-typed at some point in the future, but that's very limited and not immediately on the roadmap. `QuantumCircuit.to_instruction` does have a natural way to support both `input` vars (they become function arguments) and declared vars (since they're completely internal to the function body), but those are currently rejected because we don't have a way of specifying function signatures / calling functions yet and we don't have a neat complete story around unrolling variables into circuits yet (where there may be naming clashes), especially through transpilation. This opts to raise the errors immediately, with alternatives where possible, rather than letting things _kind of_ work. * Remove now-unused loop --- qiskit/converters/circuit_to_gate.py | 2 ++ qiskit/converters/circuit_to_instruction.py | 22 +++++++++++++ .../python/converters/test_circuit_to_gate.py | 14 ++++++++ .../converters/test_circuit_to_instruction.py | 33 +++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/qiskit/converters/circuit_to_gate.py b/qiskit/converters/circuit_to_gate.py index 283dd87dbd7..39eed1053eb 100644 --- a/qiskit/converters/circuit_to_gate.py +++ b/qiskit/converters/circuit_to_gate.py @@ -58,6 +58,8 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label if circuit.clbits: raise QiskitError("Circuit with classical bits cannot be converted to gate.") + if circuit.num_vars: + raise QiskitError("circuits with realtime classical variables cannot be converted to gates") for instruction in circuit.data: if not _check_is_gate(instruction.operation): diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index e4bba13b033..2bdcbfef358 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -61,6 +61,28 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None # pylint: disable=cyclic-import from qiskit.circuit.quantumcircuit import QuantumCircuit + if circuit.num_input_vars: + # This could be supported by moving the `input` variables to be parameters of the + # instruction, but we don't really have a good reprssentation of that yet, so safer to + # forbid it. + raise QiskitError("Circuits with 'input' variables cannot yet be converted to instructions") + if circuit.num_captured_vars: + raise QiskitError("Circuits that capture variables cannot be converted to instructions") + if circuit.num_declared_vars: + # This could very easily be supported in representations, since the variables are allocated + # and freed within the instruction itself. The reason to initially forbid it is to avoid + # needing to support unrolling such instructions within the transpiler; we would potentially + # need to remap variables to unique names in the larger context, and we don't yet have a way + # to return that information from the transpiler. We have to catch that in the transpiler + # as well since a user could manually make an instruction with such a definition, but + # forbidding it here means users get a more meaningful error at the point that the + # instruction actually gets created (since users often aren't aware that + # `QuantumCircuit.append(QuantumCircuit)` implicitly converts to an instruction). + raise QiskitError( + "Circuits with internal variables cannot yet be converted to instructions." + " You may be able to use `QuantumCircuit.compose` to inline this circuit into another." + ) + if parameter_map is None: parameter_dict = {p: p for p in circuit.parameters} else: diff --git a/test/python/converters/test_circuit_to_gate.py b/test/python/converters/test_circuit_to_gate.py index de3ad079e56..8e71a7f595a 100644 --- a/test/python/converters/test_circuit_to_gate.py +++ b/test/python/converters/test_circuit_to_gate.py @@ -18,6 +18,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import Gate, Qubit +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import Operator from qiskit.exceptions import QiskitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -122,3 +123,16 @@ def test_zero_operands(self): compound = QuantumCircuit(1) compound.append(gate, [], []) np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16) + + def test_realtime_vars_rejected(self): + """Gates can't have realtime variables.""" + qc = QuantumCircuit(1, inputs=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() + qc = QuantumCircuit(1, captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() + qc = QuantumCircuit(1) + qc.add_var("a", False) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() diff --git a/test/python/converters/test_circuit_to_instruction.py b/test/python/converters/test_circuit_to_instruction.py index 56a227dbad9..d4b69e71aa1 100644 --- a/test/python/converters/test_circuit_to_instruction.py +++ b/test/python/converters/test_circuit_to_instruction.py @@ -21,6 +21,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import Qubit, Clbit, Instruction from qiskit.circuit import Parameter +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import Operator from qiskit.exceptions import QiskitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -218,6 +219,38 @@ def test_zero_operands(self): compound.append(instruction, [], []) np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16) + def test_forbids_captured_vars(self): + """Instructions (here an analogue of functions) cannot close over outer scopes.""" + qc = QuantumCircuit(captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "Circuits that capture variables cannot"): + qc.to_instruction() + + def test_forbids_input_vars(self): + """This test can be relaxed when we have proper support for the behaviour. + + This actually has a natural meaning; the input variables could become typed parameters. + We don't have a formal structure for managing that yet, though, so it's forbidden until the + library is ready for that.""" + qc = QuantumCircuit(inputs=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "Circuits with 'input' variables cannot"): + qc.to_instruction() + + def test_forbids_declared_vars(self): + """This test can be relaxed when we have proper support for the behaviour. + + This has a very natural representation, which needs basically zero special handling, since + the variables are necessarily entirely internal to the subroutine. The reason it is + starting off as forbidden is because we don't have a good way to support variable renaming + during unrolling in transpilation, and we want the error to indicate an alternative at the + point the conversion happens.""" + qc = QuantumCircuit() + qc.add_var("a", False) + with self.assertRaisesRegex( + QiskitError, + "Circuits with internal variables.*You may be able to use `QuantumCircuit.compose`", + ): + qc.to_instruction() + if __name__ == "__main__": unittest.main(verbosity=2) From 6b73b58550da278ba0e0dbe06f851b3e623d5595 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 1 May 2024 21:12:26 +0100 Subject: [PATCH 019/159] Support `Var` in circuit-substitution methods (#12215) * Support `Var` in `QuantumCircuit.compose` This converts the operation-rewriting step of `QuantumCircuit.compose` into one that can recurse through control-flow operations rewriting variables (to avoid conflicts). This initial commit adds two ways of combining the variables: 1. variables in the `other` are disjoint from those in `self`, and are simply added to them. This makes it easy to join unrelated circuits. 2. variables marked as "captures" in `other` can be inlined onto existing variables in `self`. This makes it possible to build up a circuit with variables layer-by-layer. In support of objective 2, I also taught `copy_empty_like` a way to produce a new base layer with all the declared variables converted to "captures" to make it easier to produce new base layers. I deliberately did not include any _automatic_ variable renaming because the usability of that seemed very hard to do well; while the circuit can easily be created in a unique way, the user would then be hard-pressed to actually retrieve the new `Var` nodes afterwards. Asking the user to manually break naming collisions guarantees that they'll be able to find their variables again afterwards. * Support `Var` in `DAGCircuit.compose` This similarly adds support for a `vars_mode` argument to `DAGCircuit.copy_empty_like` to make using this more convenient. It threads the same argument through some of the `DAGCircuit` methods that build up layers of DAGs, since this is more common here. Unlike `QuantumCircuit.compose`, `DAGCircuit.compose` does not (yet?) offer the ability to remap variables during the composition, because the use-cases for direct `DAGCircuit` composition are typically less about building up many circuits from scratch and more about rebuilding a DAG from itself. Not offering the option makes it simpler to implement. * Support `Var` in `DAGCircuit.replace_block_with_op` This is straightforwards, since the wire structure is the same; there's not actually any changes needed except for a minor comment explaining that it works automatically. * Support `Var` in `DAGCircuit.substitute_node_with_dag` This is marginally trickier, and to avoid encoding performance problems for ourselves in the public API, we want to avoid any requirement for ourselves to recurse. The expectation is that use-cases of this will be building the replacement DAG themselves, where it's easy for them to arrange for the `Var`s to be the correct ones immediately. This also allows nice things like substitutions that contract wire use out completely, over all wire types, not just qubits. * Support `Var` in `DAGCircuit.substitute_node` The easiest way to do this is actually to simplify the code a whole bunch using the wire helper methods. It would have been automatically supported, had those methods already been in use. * Increase test coverage --- crates/circuit/src/circuit_data.rs | 28 +- qiskit/circuit/_classical_resource_map.py | 9 +- qiskit/circuit/library/blueprintcircuit.py | 28 +- qiskit/circuit/quantumcircuit.py | 231 ++++++++++++++--- qiskit/dagcircuit/dagcircuit.py | 205 +++++++++++---- .../python/circuit/test_circuit_operations.py | 63 +++++ test/python/circuit/test_compose.py | 163 +++++++++++- test/python/dagcircuit/test_compose.py | 88 ++++++- test/python/dagcircuit/test_dagcircuit.py | 241 ++++++++++++++++++ 9 files changed, 950 insertions(+), 106 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 07bab2c17c9..590fc07e8f8 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -459,21 +459,14 @@ impl CircuitData { clbits: Option<&Bound>, ) -> PyResult<()> { let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; - if temp.qubits_native.len() < self.qubits_native.len() { - return Err(PyValueError::new_err(format!( - "Replacement 'qubits' of size {:?} must contain at least {:?} bits.", - temp.qubits_native.len(), - self.qubits_native.len(), - ))); - } - if temp.clbits_native.len() < self.clbits_native.len() { - return Err(PyValueError::new_err(format!( - "Replacement 'clbits' of size {:?} must contain at least {:?} bits.", - temp.clbits_native.len(), - self.clbits_native.len(), - ))); - } if qubits.is_some() { + if temp.qubits_native.len() < self.qubits_native.len() { + return Err(PyValueError::new_err(format!( + "Replacement 'qubits' of size {:?} must contain at least {:?} bits.", + temp.qubits_native.len(), + self.qubits_native.len(), + ))); + } std::mem::swap(&mut temp.qubits, &mut self.qubits); std::mem::swap(&mut temp.qubits_native, &mut self.qubits_native); std::mem::swap( @@ -482,6 +475,13 @@ impl CircuitData { ); } if clbits.is_some() { + if temp.clbits_native.len() < self.clbits_native.len() { + return Err(PyValueError::new_err(format!( + "Replacement 'clbits' of size {:?} must contain at least {:?} bits.", + temp.clbits_native.len(), + self.clbits_native.len(), + ))); + } std::mem::swap(&mut temp.clbits, &mut self.clbits); std::mem::swap(&mut temp.clbits_native, &mut self.clbits_native); std::mem::swap( diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py index cfbdd077bda..454826d6035 100644 --- a/qiskit/circuit/_classical_resource_map.py +++ b/qiskit/circuit/_classical_resource_map.py @@ -37,17 +37,20 @@ class VariableMapper(expr.ExprVisitor[expr.Expr]): ``ValueError`` will be raised instead. The given ``add_register`` callable may choose to raise its own exception.""" - __slots__ = ("target_cregs", "register_map", "bit_map", "add_register") + __slots__ = ("target_cregs", "register_map", "bit_map", "var_map", "add_register") def __init__( self, target_cregs: typing.Iterable[ClassicalRegister], bit_map: typing.Mapping[Bit, Bit], + var_map: typing.Mapping[expr.Var, expr.Var] | None = None, + *, add_register: typing.Callable[[ClassicalRegister], None] | None = None, ): self.target_cregs = tuple(target_cregs) self.register_map = {} self.bit_map = bit_map + self.var_map = var_map or {} self.add_register = add_register def _map_register(self, theirs: ClassicalRegister) -> ClassicalRegister: @@ -127,9 +130,7 @@ def visit_var(self, node, /): return expr.Var(self.bit_map[node.var], node.type) if isinstance(node.var, ClassicalRegister): return expr.Var(self._map_register(node.var), node.type) - # Defensive against the expansion of the variable system; we don't want to silently do the - # wrong thing (which would be `return node` without mapping, right now). - raise RuntimeError(f"unhandled variable in 'compose': {node}") # pragma: no cover + return self.var_map.get(node, node) def visit_value(self, node, /): return expr.Value(node.value, node.type) diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 3d1f5c77f44..2bbd5ca5650 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -128,11 +128,31 @@ def _append(self, instruction, _qargs=None, _cargs=None): return super()._append(instruction, _qargs, _cargs) def compose( - self, other, qubits=None, clbits=None, front=False, inplace=False, wrap=False, *, copy=True + self, + other, + qubits=None, + clbits=None, + front=False, + inplace=False, + wrap=False, + *, + copy=True, + var_remap=None, + inline_captures=False, ): if not self._is_built: self._build() - return super().compose(other, qubits, clbits, front, inplace, wrap, copy=copy) + return super().compose( + other, + qubits, + clbits, + front, + inplace, + wrap, + copy=copy, + var_remap=var_remap, + inline_captures=False, + ) def inverse(self, annotated: bool = False): if not self._is_built: @@ -180,10 +200,10 @@ def num_connected_components(self, unitary_only=False): self._build() return super().num_connected_components(unitary_only=unitary_only) - def copy_empty_like(self, name=None): + def copy_empty_like(self, name=None, *, vars_mode="alike"): if not self._is_built: self._build() - cpy = super().copy_empty_like(name=name) + cpy = super().copy_empty_like(name=name, vars_mode=vars_mode) # The base `copy_empty_like` will typically trigger code that `BlueprintCircuit` treats as # an "invalidation", so we have to manually restore properties deleted by that that # `copy_empty_like` is supposed to propagate. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index b19269a4916..ad966b685e7 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -883,11 +883,31 @@ def compose( wrap: bool = False, *, copy: bool = True, + var_remap: Mapping[str | expr.Var, str | expr.Var] | None = None, + inline_captures: bool = False, ) -> Optional["QuantumCircuit"]: """Compose circuit with ``other`` circuit or instruction, optionally permuting wires. ``other`` can be narrower or of equal width to ``self``. + When dealing with realtime variables (:class:`.expr.Var` instances), there are two principal + strategies for using :meth:`compose`: + + 1. The ``other`` circuit is treated as entirely additive, including its variables. The + variables in ``other`` must be entirely distinct from those in ``self`` (use + ``var_remap`` to help with this), and all variables in ``other`` will be declared anew in + the output with matching input/capture/local scoping to how they are in ``other``. This + is generally what you want if you're joining two unrelated circuits. + + 2. The ``other`` circuit was created as an exact extension to ``self`` to be inlined onto + it, including acting on the existing variables in their states at the end of ``self``. + In this case, ``other`` should be created with all these variables to be inlined declared + as "captures", and then you can use ``inline_captures=True`` in this method to link them. + This is generally what you want if you're building up a circuit by defining layers + on-the-fly, or rebuilding a circuit using layers taken from itself. You might find the + ``vars_mode="captures"`` argument to :meth:`copy_empty_like` useful to create each + layer's base, in this case. + Args: other (qiskit.circuit.Instruction or QuantumCircuit): (sub)circuit or instruction to compose onto self. If not a :obj:`.QuantumCircuit`, @@ -905,6 +925,24 @@ def compose( the base circuit, in order to avoid unnecessary copies; in this case, it is not valid to use ``other`` afterwards, and some instructions may have been mutated in place. + var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` nodes in ``other`` as + they are inlined into ``self``. This can be used to avoid naming conflicts. + + Both keys and values can be given as strings or direct :class:`.expr.Var` instances. + If a key is a string, it matches any :class:`~.expr.Var` with the same name. If a + value is a string, whenever a new key matches a it, a new :class:`~.expr.Var` is + created with the correct type. If a value is a :class:`~.expr.Var`, its + :class:`~.expr.Expr.type` must exactly match that of the variable it is replacing. + inline_captures (bool): if ``True``, then all "captured" :class:`~.expr.Var` nodes in + the ``other`` :class:`.QuantumCircuit` are assumed to refer to variables already + declared in ``self`` (as any input/capture/local type), and the uses in ``other`` + will apply to the existing variables. If you want to build up a layer for an + existing circuit to use with :meth:`compose`, you might find the + ``vars_mode="captures"`` argument to :meth:`copy_empty_like` useful. Any remapping + in ``vars_remap`` occurs before evaluating this variable inlining. + + If this is ``False`` (the default), then all variables in ``other`` will be required + to be distinct from those in ``self``, and new declarations will be made for them. Returns: QuantumCircuit: the composed circuit (returns None if inplace==True). @@ -961,6 +999,31 @@ def compose( # error that the user might want to correct in an interactive session. dest = self if inplace else self.copy() + var_remap = {} if var_remap is None else var_remap + + # This doesn't use `functools.cache` so we can access it during the variable remapping of + # instructions. We cache all replacement lookups for a) speed and b) to ensure that + # the same variable _always_ maps to the same replacement even if it's used in different + # places in the recursion tree (such as being a captured variable). + def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: + # This is closing over an argument to `compose`. + nonlocal var_remap + + if out := cache.get(var): + return out + if (replacement := var_remap.get(var)) or (replacement := var_remap.get(var.name)): + if isinstance(replacement, str): + replacement = expr.Var.new(replacement, var.type) + if replacement.type != var.type: + raise CircuitError( + f"mismatched types in replacement for '{var.name}':" + f" '{var.type}' cannot become '{replacement.type}'" + ) + else: + replacement = var + cache[var] = replacement + return replacement + # As a special case, allow composing some clbits onto no clbits - normally the destination # has to be strictly larger. This allows composing final measurements onto unitary circuits. if isinstance(other, QuantumCircuit): @@ -1044,38 +1107,100 @@ def compose( dest.unit = "dt" dest.global_phase += other.global_phase - if not other.data: - # Nothing left to do. Plus, accessing 'data' here is necessary - # to trigger any lazy building since we now access '_data' - # directly. - return None if inplace else dest + # This is required to trigger data builds if the `other` is an unbuilt `BlueprintCircuit`, + # so we can the access the complete `CircuitData` object at `_data`. + _ = other.data - variable_mapper = _classical_resource_map.VariableMapper( - dest.cregs, edge_map, dest.add_register - ) + def copy_with_remapping( + source, dest, bit_map, var_map, inline_captures, new_qubits=None, new_clbits=None + ): + # Copy the instructions from `source` into `dest`, remapping variables in instructions + # according to `var_map`. If `new_qubits` or `new_clbits` are given, the qubits and + # clbits of the source instruction are remapped to those as well. + for var in source.iter_input_vars(): + dest.add_input(replace_var(var, var_map)) + if inline_captures: + for var in source.iter_captured_vars(): + replacement = replace_var(var, var_map) + if not dest.has_var(replace_var(var, var_map)): + if var is replacement: + raise CircuitError( + f"Variable '{var}' to be inlined is not in the base circuit." + " If you wanted it to be automatically added, use" + " `inline_captures=False`." + ) + raise CircuitError( + f"Replacement '{replacement}' for variable '{var}' is not in the" + " base circuit. Is the replacement correct?" + ) + else: + for var in source.iter_captured_vars(): + dest.add_capture(replace_var(var, var_map)) + for var in source.iter_declared_vars(): + dest.add_uninitialized_var(replace_var(var, var_map)) + + def recurse_block(block): + # Recurse the remapping into a control-flow block. Note that this doesn't remap the + # clbits within; the story around nested classical-register-based control-flow + # doesn't really work in the current data model, and we hope to replace it with + # `Expr`-based control-flow everywhere. + new_block = block.copy_empty_like() + new_block._vars_input = {} + new_block._vars_capture = {} + new_block._vars_local = {} + # For the recursion, we never want to inline captured variables because we're not + # copying onto a base that has variables. + copy_with_remapping(block, new_block, bit_map, var_map, inline_captures=False) + return new_block + + variable_mapper = _classical_resource_map.VariableMapper( + dest.cregs, bit_map, var_map, add_register=dest.add_register + ) - def map_vars(op): - n_op = op.copy() if copy else op - if (condition := getattr(n_op, "condition", None)) is not None: - n_op.condition = variable_mapper.map_condition(condition) - if isinstance(n_op, SwitchCaseOp): - n_op = n_op.copy() if n_op is op else n_op - n_op.target = variable_mapper.map_target(n_op.target) - return n_op + def map_vars(op): + n_op = op + is_control_flow = isinstance(n_op, ControlFlowOp) + if ( + not is_control_flow + and (condition := getattr(n_op, "condition", None)) is not None + ): + n_op = n_op.copy() if n_op is op and copy else n_op + n_op.condition = variable_mapper.map_condition(condition) + elif is_control_flow: + n_op = n_op.replace_blocks(recurse_block(block) for block in n_op.blocks) + if isinstance(n_op, (IfElseOp, WhileLoopOp)): + n_op.condition = variable_mapper.map_condition(n_op.condition) + elif isinstance(n_op, SwitchCaseOp): + n_op.target = variable_mapper.map_target(n_op.target) + elif isinstance(n_op, Store): + n_op = Store( + variable_mapper.map_expr(n_op.lvalue), variable_mapper.map_expr(n_op.rvalue) + ) + return n_op.copy() if n_op is op and copy else n_op - mapped_instrs: CircuitData = other._data.copy() - mapped_instrs.replace_bits(qubits=mapped_qubits, clbits=mapped_clbits) - mapped_instrs.map_ops(map_vars) + instructions = source._data.copy() + instructions.replace_bits(qubits=new_qubits, clbits=new_clbits) + instructions.map_ops(map_vars) + dest._current_scope().extend(instructions) append_existing = None if front: append_existing = dest._data.copy() dest.clear() - - circuit_scope = dest._current_scope() - circuit_scope.extend(mapped_instrs) + copy_with_remapping( + other, + dest, + bit_map=edge_map, + # The actual `Var: Var` map gets built up from the more freeform user input as we + # encounter the variables, since the user might be using string keys to refer to more + # than one variable in separated scopes of control-flow operations. + var_map={}, + inline_captures=inline_captures, + new_qubits=mapped_qubits, + new_clbits=mapped_clbits, + ) if append_existing: - circuit_scope.extend(append_existing) + dest._current_scope().extend(append_existing) return None if inplace else dest @@ -2520,7 +2645,7 @@ def num_tensor_factors(self) -> int: """ return self.num_unitary_factors() - def copy(self, name: str | None = None) -> "QuantumCircuit": + def copy(self, name: str | None = None) -> typing.Self: """Copy the circuit. Args: @@ -2556,24 +2681,47 @@ def memo_copy(op): ) return cpy - def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": + def copy_empty_like( + self, + name: str | None = None, + *, + vars_mode: Literal["alike", "captures", "drop"] = "alike", + ) -> typing.Self: """Return a copy of self with the same structure but empty. That structure includes: - * name, calibrations and other metadata - * global phase - * all the qubits and clbits, including the registers + + * name, calibrations and other metadata + * global phase + * all the qubits and clbits, including the registers + * the realtime variables defined in the circuit, handled according to the ``vars`` keyword + argument. .. warning:: If the circuit contains any local variable declarations (those added by the ``declarations`` argument to the circuit constructor, or using :meth:`add_var`), they - will be **uninitialized** in the output circuit. You will need to manually add store + may be **uninitialized** in the output circuit. You will need to manually add store instructions for them (see :class:`.Store` and :meth:`.QuantumCircuit.store`) to initialize them. Args: - name (str): Name for the copied circuit. If None, then the name stays the same. + name: Name for the copied circuit. If None, then the name stays the same. + vars_mode: The mode to handle realtime variables in. + + alike + The variables in the output circuit will have the same declaration semantics as + in the original circuit. For example, ``input`` variables in the source will be + ``input`` variables in the output circuit. + + captures + All variables will be converted to captured variables. This is useful when you + are building a new layer for an existing circuit that you will want to + :meth:`compose` onto the base, since :meth:`compose` can inline captures onto + the base circuit (but not other variables). + + drop + The output circuit will have no variables defined. Returns: QuantumCircuit: An empty copy of self. @@ -2591,12 +2739,23 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() - # Note that this causes the local variables to be uninitialised, because the stores are not - # copied. This can leave the circuit in a potentially dangerous state for users if they - # don't re-add initialiser stores. - cpy._vars_local = self._vars_local.copy() - cpy._vars_input = self._vars_input.copy() - cpy._vars_capture = self._vars_capture.copy() + if vars_mode == "alike": + # Note that this causes the local variables to be uninitialised, because the stores are + # not copied. This can leave the circuit in a potentially dangerous state for users if + # they don't re-add initialiser stores. + cpy._vars_local = self._vars_local.copy() + cpy._vars_input = self._vars_input.copy() + cpy._vars_capture = self._vars_capture.copy() + elif vars_mode == "captures": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {var.name: var for var in self.iter_vars()} + elif vars_mode == "drop": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {} + else: # pragma: no cover + raise ValueError(f"unknown vars_mode: '{vars_mode}'") cpy._parameter_table = ParameterTable() for parameter in getattr(cpy.global_phase, "parameters", ()): diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 6f00a3b3ea7..838e9cfe0f8 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -28,7 +28,7 @@ import math from collections import OrderedDict, defaultdict, deque, namedtuple from collections.abc import Callable, Sequence, Generator, Iterable -from typing import Any +from typing import Any, Literal import numpy as np import rustworkx as rx @@ -56,6 +56,8 @@ from qiskit.pulse import Schedule BitLocations = namedtuple("BitLocations", ("index", "registers")) +# The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. +_VarsMode = Literal["alike", "captures", "drop"] class DAGCircuit: @@ -652,7 +654,7 @@ def _decrement_op(self, op): else: self._op_names[op.name] -= 1 - def copy_empty_like(self): + def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): """Return a copy of self with the same structure but empty. That structure includes: @@ -660,7 +662,24 @@ def copy_empty_like(self): * global phase * duration * all the qubits and clbits, including the registers - * all the classical variables. + * all the classical variables, with a mode defined by ``vars_mode``. + + Args: + vars_mode: The mode to handle realtime variables in. + + alike + The variables in the output DAG will have the same declaration semantics as + in the original circuit. For example, ``input`` variables in the source will be + ``input`` variables in the output DAG. + + captures + All variables will be converted to captured variables. This is useful when you + are building a new layer for an existing DAG that you will want to + :meth:`compose` onto the base, since :meth:`compose` can inline captures onto + the base circuit (but not other variables). + + drop + The output DAG will have no variables defined. Returns: DAGCircuit: An empty copy of self. @@ -681,12 +700,20 @@ def copy_empty_like(self): for creg in self.cregs.values(): target_dag.add_creg(creg) - for var in self.iter_input_vars(): - target_dag.add_input_var(var) - for var in self.iter_captured_vars(): - target_dag.add_captured_var(var) - for var in self.iter_declared_vars(): - target_dag.add_declared_var(var) + if vars_mode == "alike": + for var in self.iter_input_vars(): + target_dag.add_input_var(var) + for var in self.iter_captured_vars(): + target_dag.add_captured_var(var) + for var in self.iter_declared_vars(): + target_dag.add_declared_var(var) + elif vars_mode == "captures": + for var in self.iter_vars(): + target_dag.add_captured_var(var) + elif vars_mode == "drop": + pass + else: # pragma: no cover + raise ValueError(f"unknown vars_mode: '{vars_mode}'") return target_dag @@ -795,7 +822,9 @@ def apply_operation_front( ) return node - def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): + def compose( + self, other, qubits=None, clbits=None, front=False, inplace=True, *, inline_captures=False + ): """Compose the ``other`` circuit onto the output of this circuit. A subset of input wires of ``other`` are mapped @@ -809,6 +838,18 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): clbits (list[Clbit|int]): clbits of self to compose onto. front (bool): If True, front composition will be performed (not implemented yet) inplace (bool): If True, modify the object. Otherwise return composed circuit. + inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG + will inlined onto existing uses of those same variables in ``self``. If ``False``, + all variables in ``other`` are required to be distinct from ``self``, and they will + be added to ``self``. + + .. + Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's + because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, + and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within + itself than to join unrelated circuits. While there's no strong motivating use-case + (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide + the option. Returns: DAGCircuit: the composed dag (returns None if inplace==True). @@ -871,27 +912,52 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): for gate, cals in other.calibrations.items(): dag._calibrations[gate].update(cals) + # This is all the handling we need for realtime variables, if there's no remapping. They: + # + # * get added to the DAG and then operations involving them get appended on normally. + # * get inlined onto an existing variable, then operations get appended normally. + # * there's a clash or a failed inlining, and we just raise an error. + # + # Notably if there's no remapping, there's no need to recurse into control-flow or to do any + # Var rewriting during the Expr visits. + for var in other.iter_input_vars(): + dag.add_input_var(var) + if inline_captures: + for var in other.iter_captured_vars(): + if not dag.has_var(var): + raise DAGCircuitError( + f"Variable '{var}' to be inlined is not in the base DAG." + " If you wanted it to be automatically added, use `inline_captures=False`." + ) + else: + for var in other.iter_captured_vars(): + dag.add_captured_var(var) + for var in other.iter_declared_vars(): + dag.add_declared_var(var) + # Ensure that the error raised here is a `DAGCircuitError` for backwards compatibility. def _reject_new_register(reg): raise DAGCircuitError(f"No register with '{reg.bits}' to map this expression onto.") variable_mapper = _classical_resource_map.VariableMapper( - dag.cregs.values(), edge_map, _reject_new_register + dag.cregs.values(), edge_map, add_register=_reject_new_register ) for nd in other.topological_nodes(): if isinstance(nd, DAGInNode): - # if in edge_map, get new name, else use existing name - m_wire = edge_map.get(nd.wire, nd.wire) - # the mapped wire should already exist - if m_wire not in dag.output_map: - raise DAGCircuitError( - "wire %s[%d] not in self" % (m_wire.register.name, m_wire.index) - ) - if nd.wire not in other._wires: - raise DAGCircuitError( - "inconsistent wire type for %s[%d] in other" - % (nd.register.name, nd.wire.index) - ) + if isinstance(nd.wire, Bit): + # if in edge_map, get new name, else use existing name + m_wire = edge_map.get(nd.wire, nd.wire) + # the mapped wire should already exist + if m_wire not in dag.output_map: + raise DAGCircuitError( + "wire %s[%d] not in self" % (m_wire.register.name, m_wire.index) + ) + if nd.wire not in other._wires: + raise DAGCircuitError( + "inconsistent wire type for %s[%d] in other" + % (nd.register.name, nd.wire.index) + ) + # If it's a Var wire, we already checked that it exists in the destination. elif isinstance(nd, DAGOutNode): # ignore output nodes pass @@ -1115,6 +1181,16 @@ def iter_declared_vars(self): """Iterable over the declared local classical variables tracked by the circuit.""" return iter(self._vars_by_type[_DAGVarType.DECLARE]) + def has_var(self, var: str | expr.Var) -> bool: + """Is this realtime variable in the DAG? + + Args: + var: the variable or name to check. + """ + if isinstance(var, str): + return var in self._vars_info + return (info := self._vars_info.get(var.name, False)) and info.var is var + def __eq__(self, other): # Try to convert to float, but in case of unbound ParameterExpressions # a TypeError will be raise, fallback to normal equality in those @@ -1220,7 +1296,8 @@ def replace_block_with_op( multiple gates in the combined single op node. If a :class:`.Bit` is not in the dictionary, it will not be added to the args; this can be useful when dealing with control-flow operations that have inherent bits in their ``condition`` or ``target`` - fields. + fields. :class:`.expr.Var` wires similarly do not need to be in this map, since + they will never be in ``qargs`` or ``cargs``. cycle_check (bool): When set to True this method will check that replacing the provided ``node_block`` with a single node would introduce a cycle (which would invalidate the @@ -1287,12 +1364,22 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit Args: node (DAGOpNode): node to substitute - input_dag (DAGCircuit): circuit that will substitute the node + input_dag (DAGCircuit): circuit that will substitute the node. wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits in the input circuit. If a list, then the bits refer to those in the ``input_dag``, and the order gets matched to the node wires by qargs first, then cargs, then conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those that the ``node`` acts on. + + Standalone :class:`~.expr.Var` nodes cannot currently be remapped as part of the + substitution; the ``input_dag`` should be defined over the correct set of variables + already. + + .. + The rule about not remapping `Var`s is to avoid performance pitfalls and reduce + complexity; the creator of the input DAG should easily be able to arrange for + the correct `Var`s to be used, and doing so avoids us needing to recurse through + control-flow operations to do deep remappings. propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on the operation within ``node`` is propagated to each node in the ``input_dag``. If ``False``, then the ``input_dag`` is assumed to faithfully implement suitable @@ -1331,12 +1418,27 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit for input_dag_wire, our_wire in wire_map.items(): if our_wire not in self.input_map: raise DAGCircuitError(f"bit mapping invalid: {our_wire} is not in this DAG") + if isinstance(our_wire, expr.Var) or isinstance(input_dag_wire, expr.Var): + raise DAGCircuitError("`Var` nodes cannot be remapped during substitution") # Support mapping indiscriminately between Qubit and AncillaQubit, etc. check_type = Qubit if isinstance(our_wire, Qubit) else Clbit if not isinstance(input_dag_wire, check_type): raise DAGCircuitError( f"bit mapping invalid: {input_dag_wire} and {our_wire} are different bit types" ) + if _may_have_additional_wires(node.op): + node_vars = {var for var in _additional_wires(node.op) if isinstance(var, expr.Var)} + else: + node_vars = set() + dag_vars = set(input_dag.iter_vars()) + if dag_vars - node_vars: + raise DAGCircuitError( + "Cannot replace a node with a DAG with more variables." + f" Variables in node: {node_vars}." + f" Variables in DAG: {dag_vars}." + ) + for var in dag_vars: + wire_map[var] = var 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 @@ -1415,14 +1517,22 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit node._node_id, lambda edge, wire=self_wire: edge == wire )[0] self._multi_graph.add_edge(pred._node_id, succ._node_id, self_wire) + for contracted_var in node_vars - dag_vars: + pred = self._multi_graph.find_predecessors_by_edge( + node._node_id, lambda edge, wire=contracted_var: edge == wire + )[0] + succ = self._multi_graph.find_successors_by_edge( + node._node_id, lambda edge, wire=contracted_var: edge == wire + )[0] + self._multi_graph.add_edge(pred._node_id, succ._node_id, contracted_var) # Exlude any nodes from in_dag that are not a DAGOpNode or are on - # bits outside the set specified by the wires kwarg + # wires outside the set specified by the wires kwarg def filter_fn(node): if not isinstance(node, DAGOpNode): return False - for qarg in node.qargs: - if qarg not in wire_map: + for _, _, wire in in_dag.edges(node): + if wire not in wire_map: return False return True @@ -1459,7 +1569,7 @@ def edge_weight_map(wire): self._decrement_op(node.op) variable_mapper = _classical_resource_map.VariableMapper( - self.cregs.values(), wire_map, self.add_creg + self.cregs.values(), wire_map, add_register=self.add_creg ) # Iterate over nodes of input_circuit and update wires in node objects migrated # from in_dag @@ -1531,21 +1641,12 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ # This might include wires that are inherent to the node, like in its `condition` or # `target` fields, so might be wider than `node.op.num_{qu,cl}bits`. current_wires = {wire for _, _, wire in self.edges(node)} - new_wires = set(node.qargs) | set(node.cargs) - if (new_condition := getattr(op, "condition", None)) is not None: - new_wires.update(condition_resources(new_condition).clbits) - elif isinstance(op, SwitchCaseOp): - if isinstance(op.target, Clbit): - new_wires.add(op.target) - elif isinstance(op.target, ClassicalRegister): - new_wires.update(op.target) - else: - new_wires.update(node_resources(op.target).clbits) + new_wires = set(node.qargs) | set(node.cargs) | set(_additional_wires(op)) if propagate_condition and not ( isinstance(node.op, ControlFlowOp) or isinstance(op, ControlFlowOp) ): - if new_condition is not None: + if getattr(op, "condition", None) is not None: raise DAGCircuitError( "Cannot propagate a condition to an operation that already has one." ) @@ -1581,13 +1682,17 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ self._decrement_op(node.op) return new_node - def separable_circuits(self, remove_idle_qubits: bool = False) -> list["DAGCircuit"]: + def separable_circuits( + self, remove_idle_qubits: bool = False, *, vars_mode: _VarsMode = "alike" + ) -> list["DAGCircuit"]: """Decompose the circuit into sets of qubits with no gates connecting them. Args: remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from the separated circuits. If ``False``, each output circuit will contain the same number of qubits as ``self``. + vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output + DAGs. See :meth:`copy_empty_like` for details on the modes. Returns: List[DAGCircuit]: The circuits resulting from separating ``self`` into sets @@ -1612,7 +1717,7 @@ def _key(x): # Create new DAGCircuit objects from each of the rustworkx subgraph objects decomposed_dags = [] for subgraph in disconnected_subgraphs: - new_dag = self.copy_empty_like() + new_dag = self.copy_empty_like(vars_mode=vars_mode) new_dag.global_phase = 0 subgraph_is_classical = True for node in rx.lexicographical_topological_sort(subgraph, key=_key): @@ -1894,7 +1999,7 @@ def front_layer(self): return op_nodes - def layers(self): + def layers(self, *, vars_mode: _VarsMode = "captures"): """Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. A layer is a circuit whose gates act on disjoint qubits, i.e., @@ -1911,6 +2016,10 @@ def layers(self): TODO: Gates that use the same cbits will end up in different layers as this is currently implemented. This may not be the desired behavior. + + Args: + vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output + DAGs. See :meth:`copy_empty_like` for details on the modes. """ graph_layers = self.multigraph_layers() try: @@ -1935,7 +2044,7 @@ def layers(self): return # Construct a shallow copy of self - new_layer = self.copy_empty_like() + new_layer = self.copy_empty_like(vars_mode=vars_mode) for node in op_nodes: # this creates new DAGOpNodes in the new_layer @@ -1950,14 +2059,18 @@ def layers(self): yield {"graph": new_layer, "partition": support_list} - def serial_layers(self): + def serial_layers(self, *, vars_mode: _VarsMode = "captures"): """Yield a layer for all gates of this circuit. A serial layer is a circuit with one gate. The layers have the same structure as in layers(). + + Args: + vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output + DAGs. See :meth:`copy_empty_like` for details on the modes. """ for next_node in self.topological_op_nodes(): - new_layer = self.copy_empty_like() + new_layer = self.copy_empty_like(vars_mode=vars_mode) # Save the support of the operation we add to the layer support_list = [] diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 48322419679..9a934d70c71 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -485,6 +485,69 @@ def test_copy_empty_variables(self): self.assertEqual({b, d}, set(copied.iter_captured_vars())) self.assertEqual({b}, set(qc.iter_captured_vars())) + def test_copy_empty_variables_alike(self): + """Test that an empty copy of circuits including variables copies them across, but does not + initialise them. This is the same as the default, just spelled explicitly.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="alike") + self.assertEqual({a}, set(copied.iter_input_vars())) + self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_input(b) + copied.add_var(d, 0xFF) + self.assertEqual({a, b}, set(copied.iter_input_vars())) + self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({c}, set(qc.iter_declared_vars())) + + qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) + copied = qc.copy_empty_like(vars_mode="alike") + self.assertEqual({b}, set(copied.iter_captured_vars())) + self.assertEqual({a, c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_capture(d) + self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_captured_vars())) + + def test_copy_empty_variables_to_captures(self): + """``vars_mode="captures"`` should convert all variables to captures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="captures") + self.assertEqual({a, b, c}, set(copied.iter_captured_vars())) + self.assertEqual({a, b, c}, set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + + qc = QuantumCircuit(captures=[c, d]) + copied = qc.copy_empty_like(vars_mode="captures") + self.assertEqual({c, d}, set(copied.iter_captured_vars())) + self.assertEqual({c, d}, set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + + def test_copy_empty_variables_drop(self): + """``vars_mode="drop"`` should not have variables in the output.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + + qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="drop") + self.assertEqual(set(), set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + def test_copy_empty_like_parametric_phase(self): """Test that the parameter table of an empty circuit remains valid after copying a circuit with a parametric global phase.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 03301899a6a..0e481c12b33 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -34,7 +34,7 @@ CircuitError, ) from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -901,6 +901,118 @@ def test_expr_target_is_mapped(self): self.assertEqual(dest, expected) + def test_join_unrelated_vars(self): + """Composing disjoint sets of vars should produce an additive output.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + base = QuantumCircuit(inputs=[a]) + other = QuantumCircuit(inputs=[b]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a, b}, set(out.iter_input_vars())) + # Assert that base was unaltered. + self.assertEqual({a}, set(base.iter_vars())) + + base = QuantumCircuit(captures=[a]) + other = QuantumCircuit(captures=[b]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a, b}, set(out.iter_captured_vars())) + self.assertEqual({a}, set(base.iter_vars())) + + base = QuantumCircuit(inputs=[a]) + other = QuantumCircuit(declarations=[(b, 255)]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a}, set(out.iter_input_vars())) + self.assertEqual({b}, set(out.iter_declared_vars())) + + def test_var_remap_to_avoid_collisions(self): + """We can use `var_remap` to avoid a variable collision.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + base = QuantumCircuit(inputs=[a1]) + other = QuantumCircuit(inputs=[a2]) + + out = base.compose(other, var_remap={a2: b}) + self.assertEqual([a1, b], list(out.iter_input_vars())) + self.assertEqual([a1, b], list(out.iter_vars())) + + out = base.compose(other, var_remap={"a": b}) + self.assertEqual([a1, b], list(out.iter_input_vars())) + self.assertEqual([a1, b], list(out.iter_vars())) + + out = base.compose(other, var_remap={"a": "c"}) + self.assertTrue(out.has_var("c")) + c = out.get_var("c") + self.assertEqual(c.name, "c") + self.assertEqual([a1, c], list(out.iter_input_vars())) + self.assertEqual([a1, c], list(out.iter_vars())) + + def test_simple_inline_captures(self): + """We should be able to inline captures onto other variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + base = QuantumCircuit(inputs=[a, b]) + base.add_var(c, 255) + base.store(a, expr.logic_or(a, b)) + other = QuantumCircuit(captures=[a, b, c]) + other.store(c, 254) + other.store(b, expr.logic_or(a, b)) + new = base.compose(other, inline_captures=True) + + expected = QuantumCircuit(inputs=[a, b]) + expected.add_var(c, 255) + expected.store(a, expr.logic_or(a, b)) + expected.store(c, 254) + expected.store(b, expr.logic_or(a, b)) + self.assertEqual(new, expected) + + def test_can_inline_a_capture_after_remapping(self): + """We can use `var_remap` to redefine a capture variable _and then_ inline it in deeply + nested scopes. This is a stress test of capture inlining.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + # We shouldn't be able to inline `qc`'s variable use as-is because it closes over the wrong + # variable, but it should work after variable remapping. (This isn't expected to be super + # useful, it's just a consequence of how the order between `var_remap` and `inline_captures` + # is defined). + base = QuantumCircuit(inputs=[a]) + qc = QuantumCircuit(declarations=[(c, 255)], captures=[b]) + qc.store(b, expr.logic_and(b, b)) + with qc.if_test(expr.logic_not(b)): + with qc.while_loop(b): + qc.store(b, expr.logic_not(b)) + # Note that 'c' is captured in this scope, so this is also a test that 'inline_captures' + # doesn't do something silly in nested scopes. + with qc.switch(c) as case: + with case(0): + qc.store(c, expr.bit_and(c, 255)) + with case(case.DEFAULT): + qc.store(b, expr.equal(c, 255)) + base.compose(qc, inplace=True, inline_captures=True, var_remap={b: a}) + + expected = QuantumCircuit(inputs=[a], declarations=[(c, 255)]) + expected.store(a, expr.logic_and(a, a)) + with expected.if_test(expr.logic_not(a)): + with expected.while_loop(a): + expected.store(a, expr.logic_not(a)) + # Note that 'c' is not remapped. + with expected.switch(c) as case: + with case(0): + expected.store(c, expr.bit_and(c, 255)) + with case(case.DEFAULT): + expected.store(a, expr.equal(c, 255)) + + self.assertEqual(base, expected) + def test_rejects_duplicate_bits(self): """Test that compose rejects duplicates in either qubits or clbits.""" base = QuantumCircuit(5, 5) @@ -911,6 +1023,55 @@ def test_rejects_duplicate_bits(self): with self.assertRaisesRegex(CircuitError, "Duplicate clbits"): base.compose(attempt, [0, 1], [1, 1]) + def test_cannot_mix_inputs_and_captures(self): + """The rules about mixing `input` and `capture` vars should still apply.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "circuits with input variables cannot be"): + QuantumCircuit(inputs=[a]).compose(QuantumCircuit(captures=[b])) + with self.assertRaisesRegex(CircuitError, "circuits to be enclosed with captures cannot"): + QuantumCircuit(captures=[a]).compose(QuantumCircuit(inputs=[b])) + + def test_reject_var_naming_collision(self): + """We can't have multiple vars with the same name.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + self.assertNotEqual(a1, a2) + + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(inputs=[a1]).compose(QuantumCircuit(inputs=[a2])) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(captures=[a1]).compose(QuantumCircuit(declarations=[(a2, False)])) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(declarations=[(a1, True)]).compose( + QuantumCircuit(inputs=[b]), var_remap={b: a2} + ) + + def test_reject_remap_var_to_bad_type(self): + """Can't map a var to a different type.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "mismatched types"): + QuantumCircuit().compose(qc, var_remap={a: b}) + qc = QuantumCircuit(captures=[b]) + with self.assertRaisesRegex(CircuitError, "mismatched types"): + QuantumCircuit().compose(qc, var_remap={b: a}) + + def test_reject_inlining_missing_var(self): + """Can't inline a var that doesn't exist.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Variable '.*' to be inlined is not in the base"): + QuantumCircuit().compose(qc, inline_captures=True) + + # 'a' _would_ be present, except we also say to remap it before attempting the inline. + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Replacement '.*' for variable '.*' is not in"): + QuantumCircuit(inputs=[a]).compose(qc, var_remap={a: b}, inline_captures=True) + if __name__ == "__main__": unittest.main() diff --git a/test/python/dagcircuit/test_compose.py b/test/python/dagcircuit/test_compose.py index c2862eb200f..ff5014eacef 100644 --- a/test/python/dagcircuit/test_compose.py +++ b/test/python/dagcircuit/test_compose.py @@ -22,9 +22,10 @@ WhileLoopOp, SwitchCaseOp, CASE_DEFAULT, + Store, ) from qiskit.circuit.classical import expr, types -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGCircuitError from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.pulse import Schedule from qiskit.circuit.gate import Gate @@ -540,6 +541,91 @@ def test_compose_expr_target(self): self.assertEqual(dest, circuit_to_dag(expected)) + def test_join_unrelated_dags(self): + """This isn't expected to be common, but should work anyway.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_declared_var(b) + source.add_input_var(c) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_declared_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_join_unrelated_dags_captures(self): + """This isn't expected to be common, but should work anyway.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + dest = DAGCircuit() + dest.add_captured_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_declared_var(b) + source.add_captured_var(c) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source, inline_captures=False) + + expected = DAGCircuit() + expected.add_captured_var(a) + expected.add_declared_var(b) + expected.add_captured_var(c) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_inline_capture_var(self): + """Should be able to append uses onto another DAG.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.add_input_var(b) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_captured_var(b) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source, inline_captures=True) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_reject_inline_to_nonexistent_var(self): + """Should not be able to inline a variable that doesn't exist.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_captured_var(b) + with self.assertRaisesRegex( + DAGCircuitError, "Variable '.*' to be inlined is not in the base DAG" + ): + dest.compose(source, inline_captures=True) + def test_compose_calibrations(self): """Test that compose carries over the calibrations.""" dag_cal = QuantumCircuit(1) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 62172d084ad..14033e522c6 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -439,6 +439,51 @@ def test_copy_empty_like_vars(self): dag.add_declared_var(expr.Var.new("d", types.Uint(8))) self.assertEqual(dag, dag.copy_empty_like()) + def test_copy_empty_like_vars_captures(self): + """Variables can be converted to captures as part of the empty copy.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + all_captures = DAGCircuit() + for var in [a, b, c, d]: + all_captures.add_captured_var(var) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(all_captures, dag.copy_empty_like(vars_mode="captures")) + + dag = DAGCircuit() + dag.add_captured_var(a) + dag.add_captured_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(all_captures, dag.copy_empty_like(vars_mode="captures")) + + def test_copy_empty_like_vars_drop(self): + """Variables can be dropped as part of the empty copy.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(DAGCircuit(), dag.copy_empty_like(vars_mode="drop")) + + dag = DAGCircuit() + dag.add_captured_var(a) + dag.add_captured_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(DAGCircuit(), dag.copy_empty_like(vars_mode="drop")) + def test_remove_busy_clbit(self): """Classical bit removal of busy classical bits raises.""" self.dag.apply_operation_back(Measure(), [self.qreg[0]], [self.individual_clbit]) @@ -2255,6 +2300,125 @@ def test_substitute_dag_switch_expr(self): self.assertEqual(src, expected) + def test_substitute_dag_vars(self): + """Should be possible to replace a node with a DAG acting on the same wires.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_input_var(c) + dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + node = dag.apply_operation_back(Store(a, expr.logic_or(expr.logic_or(a, b), c)), (), ()) + dag.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + replace = DAGCircuit() + replace.add_captured_var(a) + replace.add_captured_var(b) + replace.add_captured_var(c) + replace.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()) + replace.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(c, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + dag.substitute_node_with_dag(node, replace, wires={}) + + self.assertEqual(dag, expected) + + def test_substitute_dag_if_else_expr_var(self): + """Test that substitution works with if/else ops with standalone Vars.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + body_rep = QuantumCircuit(1) + body_rep.z(0) + + q_rep = QuantumRegister(1) + c_rep = ClassicalRegister(2) + replacement = DAGCircuit() + replacement.add_qreg(q_rep) + replacement.add_creg(c_rep) + replacement.add_captured_var(b) + replacement.apply_operation_back(XGate(), [q_rep[0]], []) + replacement.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c_rep, 1)), body_rep, None), [q_rep[0]], [] + ) + + true_src = QuantumCircuit(1) + true_src.x(0) + true_src.z(0) + false_src = QuantumCircuit(1) + false_src.x(0) + q_src = QuantumRegister(4) + c1_src = ClassicalRegister(2) + c2_src = ClassicalRegister(2) + src = DAGCircuit() + src.add_qreg(q_src) + src.add_creg(c1_src) + src.add_creg(c2_src) + src.add_input_var(a) + src.add_input_var(b) + node = src.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c1_src, 1)), true_src, false_src), [q_src[2]], [] + ) + + wires = {q_rep[0]: q_src[2], c_rep[0]: c1_src[0], c_rep[1]: c1_src[1]} + src.substitute_node_with_dag(node, replacement, wires=wires) + + expected = DAGCircuit() + expected.add_qreg(q_src) + expected.add_creg(c1_src) + expected.add_creg(c2_src) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(XGate(), [q_src[2]], []) + expected.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c1_src, 1)), body_rep, None), [q_src[2]], [] + ) + + self.assertEqual(src, expected) + + def test_contract_var_use_to_nothing(self): + """The replacement DAG can drop wires.""" + a = expr.Var.new("a", types.Bool()) + + src = DAGCircuit() + src.add_input_var(a) + node = src.apply_operation_back(Store(a, a), (), ()) + replace = DAGCircuit() + src.substitute_node_with_dag(node, replace, {}) + + expected = DAGCircuit() + expected.add_input_var(a) + + self.assertEqual(src, expected) + + def test_raise_if_var_mismatch(self): + """The DAG can't add more wires.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + src = DAGCircuit() + src.add_input_var(a) + node = src.apply_operation_back(Store(a, a), (), ()) + + replace = DAGCircuit() + replace.add_input_var(a) + replace.add_input_var(b) + replace.apply_operation_back(Store(a, b), (), ()) + + with self.assertRaisesRegex(DAGCircuitError, "Cannot replace a node with a DAG with more"): + src.substitute_node_with_dag(node, replace, wires={}) + def test_raise_if_substituting_dag_modifies_its_conditional(self): """Verify that we raise if the input dag modifies any of the bits in node.op.condition.""" @@ -2645,6 +2809,55 @@ def test_reject_replace_switch_with_other_resources(self, inplace): node, SwitchCaseOp(expr.lift(cr2), [((1, 3), case.copy())]), inplace=inplace ) + @data(True, False) + def test_replace_switch_case_standalone_var(self, inplace): + """Replace a standalone-Var switch/case with another.""" + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(8)) + + case = QuantumCircuit(1) + case.x(0) + + qr = QuantumRegister(1) + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_input_var(a) + dag.add_input_var(b) + node = dag.apply_operation_back(SwitchCaseOp(a, [((1, 3), case.copy())]), qr, []) + dag.substitute_node( + node, SwitchCaseOp(expr.bit_and(a, 1), [(1, case.copy())]), inplace=inplace + ) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(SwitchCaseOp(expr.bit_and(a, 1), [(1, case.copy())]), qr, []) + + self.assertEqual(dag, expected) + + @data(True, False) + def test_replace_store_standalone_var(self, inplace): + """Replace a standalone-Var Store with another.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + qr = QuantumRegister(1) + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_input_var(a) + dag.add_input_var(b) + node = dag.apply_operation_back(Store(a, a), (), ()) + dag.substitute_node(node, Store(a, expr.logic_not(a)), inplace=inplace) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(Store(a, expr.logic_not(a)), (), ()) + + self.assertEqual(dag, expected) + class TestReplaceBlock(QiskitTestCase): """Test replacing a block of nodes in a DAG.""" @@ -2729,6 +2942,34 @@ def test_replace_control_flow_block(self): self.assertEqual(dag, expected) + def test_contract_stores(self): + """Test that contraction over nodes with `Var` wires works.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_input_var(c) + dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + nodes = [ + dag.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()), + dag.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()), + ] + dag.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dag.replace_block_with_op(nodes, Store(a, expr.logic_or(expr.logic_or(a, b), c)), {}) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(c, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(expr.logic_or(a, b), c)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dag, expected) + class TestDagProperties(QiskitTestCase): """Test the DAG properties.""" From a78c94144c59c504a8f36ab9786a404a572aff20 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 1 May 2024 23:49:24 +0100 Subject: [PATCH 020/159] Add minimal support for standalone `Var` to visualisers (#12307) * Add minimal support for standalone `Var` to visualisers This adds best-effort only support to the visualisers for handling stand-alone `Var` nodes. Most of the changes are actually in `qasm3`, since the visualisers use internal details of that to handle the nodes. This commit decouples the visualisers _slightly_ more from the inner workings of the OQ3 exporter by having them manage their own variable-naming contexts and using the encapsulated `_ExprBuilder`, rather than poking into random internals of the full circuit exporter. This is necessary to allow the OQ3 exporter to expand to support these variables itself, and also for the visualisers, since variables may now be introduced in inner scopes. This commit does not attempt to solve many of the known problems around zero-operand "gates", of which `Store` is one, just leaving it un-drawn. Printing to OpenQASM 3 is possibly a better visualisation strategy for large dynamic circuits for the time being. * Fix typos Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/qasm3/ast.py | 11 +++ qiskit/qasm3/exporter.py | 20 +++-- qiskit/qasm3/printer.py | 8 ++ qiskit/visualization/circuit/matplotlib.py | 56 ++++++++++--- qiskit/visualization/circuit/text.py | 56 +++++++++---- .../visualization/test_circuit_text_drawer.py | 79 +++++++++++++++++- .../references/if_else_standalone_var.png | Bin 0 -> 17114 bytes .../references/switch_standalone_var.png | Bin 0 -> 22166 bytes .../circuit/test_circuit_matplotlib_drawer.py | 55 +++++++++++- 9 files changed, 243 insertions(+), 42 deletions(-) create mode 100644 test/visual/mpl/circuit/references/if_else_standalone_var.png create mode 100644 test/visual/mpl/circuit/references/switch_standalone_var.png diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index fd7aa11d481..7674eace89d 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -123,6 +123,10 @@ class FloatType(ClassicalType, enum.Enum): OCT = 256 +class BoolType(ClassicalType): + """Type information for a Boolean.""" + + class IntType(ClassicalType): """Type information for a signed integer.""" @@ -130,6 +134,13 @@ def __init__(self, size: Optional[int] = None): self.size = size +class UintType(ClassicalType): + """Type information for an unsigned integer.""" + + def __init__(self, size: Optional[int] = None): + self.size = size + + class BitType(ClassicalType): """Type information for a single bit.""" diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index d8d3a42087a..329d09830ac 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1058,6 +1058,14 @@ def _lift_condition(condition): return expr.lift_legacy_condition(condition) +def _build_ast_type(type_: types.Type) -> ast.ClassicalType: + if type_.kind is types.Bool: + return ast.BoolType() + if type_.kind is types.Uint: + return ast.UintType(type_.width) + raise RuntimeError(f"unhandled expr type '{type_}'") # pragma: no cover + + class _ExprBuilder(expr.ExprVisitor[ast.Expression]): __slots__ = ("lookup",) @@ -1069,7 +1077,7 @@ def __init__(self, lookup): self.lookup = lookup def visit_var(self, node, /): - return self.lookup(node.var) + return self.lookup(node) if node.standalone else self.lookup(node.var) def visit_value(self, node, /): if node.type.kind is types.Bool: @@ -1080,14 +1088,8 @@ def visit_value(self, node, /): def visit_cast(self, node, /): if node.implicit: - return node.accept(self) - if node.type.kind is types.Bool: - oq3_type = ast.BoolType() - elif node.type.kind is types.Uint: - oq3_type = ast.BitArrayType(node.type.width) - else: - raise RuntimeError(f"unhandled cast type '{node.type}'") - return ast.Cast(oq3_type, node.operand.accept(self)) + return node.operand.accept(self) + return ast.Cast(_build_ast_type(node.type), node.operand.accept(self)) def visit_unary(self, node, /): return ast.Unary(ast.Unary.Op[node.op.name], node.operand.accept(self)) diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 94d12a7ecff..ba253144a16 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -204,11 +204,19 @@ def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDecla def _visit_FloatType(self, node: ast.FloatType) -> None: self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]") + def _visit_BoolType(self, _node: ast.BoolType) -> None: + self.stream.write("bool") + def _visit_IntType(self, node: ast.IntType) -> None: self.stream.write("int") if node.size is not None: self.stream.write(f"[{node.size}]") + def _visit_UintType(self, node: ast.UintType) -> None: + self.stream.write("uint") + if node.size is not None: + self.stream.write(f"[{node.size}]") + def _visit_BitType(self, _node: ast.BitType) -> None: self.stream.write("bit") diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 8d83fecb896..c547846acc5 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -33,6 +33,7 @@ IfElseOp, ForLoopOp, SwitchCaseOp, + CircuitError, ) from qiskit.circuit.controlflow import condition_resources from qiskit.circuit.classical import expr @@ -46,7 +47,8 @@ XGate, ZGate, ) -from qiskit.qasm3.exporter import QASM3Builder +from qiskit.qasm3 import ast +from qiskit.qasm3.exporter import _ExprBuilder from qiskit.qasm3.printer import BasicPrinter from qiskit.circuit.tools.pi_check import pi_check @@ -393,7 +395,7 @@ def draw(self, filename=None, verbose=False): matplotlib_close_if_inline(mpl_figure) return mpl_figure - def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, builder=None): + def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data): """Compute the layer_widths for the layers""" layer_widths = {} @@ -482,18 +484,41 @@ def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, build if (isinstance(op, SwitchCaseOp) and isinstance(op.target, expr.Expr)) or ( getattr(op, "condition", None) and isinstance(op.condition, expr.Expr) ): - condition = op.target if isinstance(op, SwitchCaseOp) else op.condition - if builder is None: - builder = QASM3Builder( - outer_circuit, - includeslist=("stdgates.inc",), - basis_gates=("U",), - disable_constants=False, - allow_aliasing=False, + + def lookup_var(var): + """Look up a classical-expression variable or register/bit in our + internal symbol table, and return an OQ3-like identifier.""" + # We don't attempt to disambiguate anything like register/var naming + # collisions; we already don't really show classical variables. + if isinstance(var, expr.Var): + return ast.Identifier(var.name) + if isinstance(var, ClassicalRegister): + return ast.Identifier(var.name) + # Single clbit. This is not actually the correct way to lookup a bit on + # the circuit (it doesn't handle bit bindings fully), but the mpl + # drawer doesn't completely track inner-outer _bit_ bindings, only + # inner-indices, so we can't fully recover the information losslessly. + # Since most control-flow uses the control-flow builders, we should + # decay to something usable most of the time. + try: + register, bit_index, reg_index = get_bit_reg_index( + outer_circuit, var + ) + except CircuitError: + # We failed to find the bit due to binding problems - fall back to + # something that's probably wrong, but at least disambiguating. + return ast.Identifier(f"bit{wire_map[var]}") + if register is None: + return ast.Identifier(f"bit{bit_index}") + return ast.SubscriptedIdentifier( + register.name, ast.IntegerLiteral(reg_index) ) - builder.build_classical_declarations() + + condition = op.target if isinstance(op, SwitchCaseOp) else op.condition stream = StringIO() - BasicPrinter(stream, indent=" ").visit(builder.build_expression(condition)) + BasicPrinter(stream, indent=" ").visit( + condition.accept(_ExprBuilder(lookup_var)) + ) expr_text = stream.getvalue() # Truncate expr_text so that first gate is no more than about 3 x_index's over if len(expr_text) > self._expr_len: @@ -570,7 +595,7 @@ def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, build # Recursively call _get_layer_widths for the circuit inside the ControlFlowOp flow_widths = flow_drawer._get_layer_widths( - node_data, flow_wire_map, outer_circuit, glob_data, builder + node_data, flow_wire_map, outer_circuit, glob_data ) layer_widths.update(flow_widths) @@ -1243,6 +1268,11 @@ def _condition(self, node, node_data, wire_map, outer_circuit, cond_xy, glob_dat self._ax.add_patch(box) xy_plot.append(xy) + if not xy_plot: + # Expression that's only on new-style `expr.Var` nodes, and doesn't need any vertical + # line drawing. + return + qubit_b = min(node_data[node].q_xy, key=lambda xy: xy[1]) clbit_b = min(xy_plot, key=lambda xy: xy[1]) diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index 1e6137275a9..abefe551177 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -20,7 +20,7 @@ import collections import sys -from qiskit.circuit import Qubit, Clbit, ClassicalRegister +from qiskit.circuit import Qubit, Clbit, ClassicalRegister, CircuitError from qiskit.circuit import ControlledGate, Reset, Measure from qiskit.circuit import ControlFlowOp, WhileLoopOp, IfElseOp, ForLoopOp, SwitchCaseOp from qiskit.circuit.classical import expr @@ -28,8 +28,9 @@ from qiskit.circuit.library.standard_gates import IGate, RZZGate, SwapGate, SXGate, SXdgGate from qiskit.circuit.annotated_operation import _canonicalize_modifiers, ControlModifier from qiskit.circuit.tools.pi_check import pi_check -from qiskit.qasm3.exporter import QASM3Builder +from qiskit.qasm3 import ast from qiskit.qasm3.printer import BasicPrinter +from qiskit.qasm3.exporter import _ExprBuilder from ._utils import ( get_gate_ctrl_text, @@ -748,7 +749,6 @@ def __init__( self._nest_depth = 0 # nesting depth for control flow ops self._expr_text = "" # expression text to display - self._builder = None # QASM3Builder class instance for expressions # Because jupyter calls both __repr__ and __repr_html__ for some backends, # the entire drawer can be run twice which can result in different output @@ -1306,25 +1306,44 @@ def add_control_flow(self, node, layers, wire_map): if (isinstance(node.op, SwitchCaseOp) and isinstance(node.op.target, expr.Expr)) or ( getattr(node.op, "condition", None) and isinstance(node.op.condition, expr.Expr) ): + + def lookup_var(var): + """Look up a classical-expression variable or register/bit in our internal symbol + table, and return an OQ3-like identifier.""" + # We don't attempt to disambiguate anything like register/var naming collisions; we + # already don't really show classical variables. + if isinstance(var, expr.Var): + return ast.Identifier(var.name) + if isinstance(var, ClassicalRegister): + return ast.Identifier(var.name) + # Single clbit. This is not actually the correct way to lookup a bit on the + # circuit (it doesn't handle bit bindings fully), but the text drawer doesn't + # completely track inner-outer _bit_ bindings, only inner-indices, so we can't fully + # recover the information losslessly. Since most control-flow uses the control-flow + # builders, we should decay to something usable most of the time. + try: + register, bit_index, reg_index = get_bit_reg_index(self._circuit, var) + except CircuitError: + # We failed to find the bit due to binding problems - fall back to something + # that's probably wrong, but at least disambiguating. + return ast.Identifier(f"_bit{wire_map[var]}") + if register is None: + return ast.Identifier(f"_bit{bit_index}") + return ast.SubscriptedIdentifier(register.name, ast.IntegerLiteral(reg_index)) + condition = node.op.target if isinstance(node.op, SwitchCaseOp) else node.op.condition - if self._builder is None: - self._builder = QASM3Builder( - self._circuit, - includeslist=("stdgates.inc",), - basis_gates=("U",), - disable_constants=False, - allow_aliasing=False, - ) - self._builder.build_classical_declarations() + draw_conditional = bool(node_resources(condition).clbits) stream = StringIO() - BasicPrinter(stream, indent=" ").visit(self._builder.build_expression(condition)) + BasicPrinter(stream, indent=" ").visit(condition.accept(_ExprBuilder(lookup_var))) self._expr_text = stream.getvalue() # Truncate expr_text at 30 chars or user-set expr_len if len(self._expr_text) > self.expr_len: self._expr_text = self._expr_text[: self.expr_len] + "..." + else: + draw_conditional = not isinstance(node.op, ForLoopOp) # # Draw a left box such as If, While, For, and Switch - flow_layer = self.draw_flow_box(node, wire_map, CF_LEFT) + flow_layer = self.draw_flow_box(node, wire_map, CF_LEFT, conditional=draw_conditional) layers.append(flow_layer.full_layer) # Get the list of circuits in the ControlFlowOp from the node blocks @@ -1351,7 +1370,9 @@ def add_control_flow(self, node, layers, wire_map): if circ_num > 0: # Draw a middle box such as Else and Case - flow_layer = self.draw_flow_box(node, flow_wire_map, CF_MID, circ_num - 1) + flow_layer = self.draw_flow_box( + node, flow_wire_map, CF_MID, circ_num - 1, conditional=False + ) layers.append(flow_layer.full_layer) _, _, nodes = _get_layered_instructions(circuit, wire_map=flow_wire_map) @@ -1380,14 +1401,13 @@ def add_control_flow(self, node, layers, wire_map): layers.append(flow_layer2.full_layer) # Draw the right box for End - flow_layer = self.draw_flow_box(node, flow_wire_map, CF_RIGHT) + flow_layer = self.draw_flow_box(node, flow_wire_map, CF_RIGHT, conditional=False) layers.append(flow_layer.full_layer) - def draw_flow_box(self, node, flow_wire_map, section, circ_num=0): + def draw_flow_box(self, node, flow_wire_map, section, circ_num=0, conditional=False): """Draw the left, middle, or right of a control flow box""" op = node.op - conditional = section == CF_LEFT and not isinstance(op, ForLoopOp) depth = str(self._nest_depth) if section == CF_LEFT: etext = "" diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 9b34257f567..5f72a7d1bbb 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -12,6 +12,9 @@ """circuit_drawer with output="text" draws a circuit in ascii art""" +# Sometimes we want to test long-lined output. +# pylint: disable=line-too-long + import pathlib import os import tempfile @@ -37,7 +40,7 @@ from qiskit.visualization import circuit_drawer from qiskit.visualization.circuit import text as elements from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( HGate, U2Gate, @@ -6316,6 +6319,80 @@ def test_switch_with_expression(self): expected, ) + def test_nested_if_else_op_var(self): + """Test if/else with standalone Var.""" + expected = "\n".join( + [ + " ┌───────── ┌──────────────── ───────┐ ┌──────────────────── ┌───┐ ───────┐ ───────┐ ", + "q_0: ┤ ┤ ──■── ├─┤ If-1 c && a == 128 ┤ H ├ End-1 ├─ ├─", + " │ If-0 !b │ If-1 b == c[0] ┌─┴─┐ End-1 │ └──────────────────── └───┘ ───────┘ End-0 │ ", + "q_1: ┤ ┤ ┤ X ├ ├────────────────────────────────────── ├─", + " └───────── └───────╥──────── └───┘ ───────┘ ───────┘ ", + " ┌───╨────┐ ", + "c: 2/═══════════════╡ [expr] ╞══════════════════════════════════════════════════════════════════", + " └────────┘ ", + ] + ) + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", False) + qc.store(a, 128) + with qc.if_test(expr.logic_not(b)): + # Mix old-style and new-style. + with qc.if_test(expr.equal(b, qc.clbits[0])): + qc.cx(0, 1) + c = qc.add_var("c", b) + with qc.if_test(expr.logic_and(c, expr.equal(a, 128))): + qc.h(0) + + actual = str(qc.draw("text", fold=-1, initial_state=False)) + self.assertEqual(actual, expected) + + def test_nested_switch_op_var(self): + """Test switch with standalone Var.""" + expected = "\n".join( + [ + " ┌───────────── ┌──────────── ┌──────────── ┌──────────── »", + "q_0: ┤ ┤ ┤ ┤ ──■──»", + " │ Switch-0 ~a │ Case-0 (0) │ Switch-1 b │ Case-1 (2) ┌─┴─┐»", + "q_1: ┤ ┤ ┤ ┤ ┤ X ├»", + " └───────────── └──────────── └──────────── └──────────── └───┘»", + "c: 2/══════════════════════════════════════════════════════════════»", + " »", + "« ┌──────────────── ┌───┐ ───────┐ ┌──────────────── ┌──────── ┌───┐»", + "«q_0: ┤ ┤ X ├ ├─┤ ┤ If-1 c ┤ H ├»", + "« │ Case-1 default └─┬─┘ End-1 │ │ Case-0 default └──────── └───┘»", + "«q_1: ┤ ──■── ├─┤ ───────────────»", + "« └──────────────── ───────┘ └──────────────── »", + "«c: 2/══════════════════════════════════════════════════════════════════»", + "« »", + "« ───────┐ ───────┐ ", + "«q_0: End-1 ├─ ├─", + "« ───────┘ End-0 │ ", + "«q_1: ────────── ├─", + "« ───────┘ ", + "«c: 2/════════════════════", + "« ", + ] + ) + + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", expr.lift(5, a.type)) + with qc.switch(expr.bit_not(a)) as case: + with case(0): + with qc.switch(b) as case2: + with case2(2): + qc.cx(0, 1) + with case2(case2.DEFAULT): + qc.cx(1, 0) + with case(case.DEFAULT): + c = qc.add_var("c", expr.equal(a, b)) + with qc.if_test(c): + qc.h(0) + actual = str(qc.draw("text", fold=80, initial_state=False)) + self.assertEqual(actual, expected) + class TestCircuitAnnotatedOperations(QiskitVisualizationTestCase): """Test AnnotatedOperations and other non-Instructions.""" diff --git a/test/visual/mpl/circuit/references/if_else_standalone_var.png b/test/visual/mpl/circuit/references/if_else_standalone_var.png new file mode 100644 index 0000000000000000000000000000000000000000..6266a0caeb00ee4dc8d2aa8bf53c197a8ff76d51 GIT binary patch literal 17114 zcmeIacTkh-|1BCo#0G9fKvZBWARtwwBVa?N_ufQ`(vcQAxK-SWf^-2Xg0#?kCrFVd zASHwriu95oy_5TSzrQnQZks#j%$z^&ow?5JiD2^Pd7t-F)@QBt?04#_k7!R_I)y@^ zXcZnm&_tmqmr*DR>l4S}H)2!W?Nw%PfP{bu-`~&O+?-vd1O*-b`x^wDT%HNCak9I^MNT?DHgH9un9d{rQ9MiPg|E8z zNa4XeKLwQkS5VhLUOr!Z$pSAzH~;?+{%3P$i6lz(7em5b3OqbTX=zHau_S?vhQc9xDfD%f zA)C3#glHZ+$HZ!-d(>9?d~38D5l-rXl$hgfzlk$NEnUj0db+kjMSa3iF%e5eE!0m0 z80E}%ahW%;tOE<5uWDPnTs%Uielx{HW=CPl-eCYo=a77s;!7GROJ z=$qt-?=2>nocolNAERCU{;QPE8Tr?bO&J>(Lc?6T(y?8GZaW`Tcs?+oc4CqhO)(>R zFH=a%xfpRbA$d=6d$G&!PFi(UJxj_-I*R#3x~?0qQXM+7@r?2+OXAc$Pbb&dt8y#7 zG~0uo*-zEZ^-^3gQ|A;Et*T4Q`@H_z=#}(K7WlMJ=-PEGpOsZ*b>yiw+hQA~(cJ<% z&lF75x1~6JMTTKCOZj5CMa^+kq;iCZJMJiW!SOVa#5VP)@}WLjutBQ*)h*`Uy+d8A zndoINWqlfT!~OE3H2(4r5jb1-jw$*<4eJw2BrEou)GItT&$)7_oEgzU9+cAUuRE=^ z#=dNyTyk_1)_Lt7$@@(B+hN9g@)y;PqH_Zj61bz>03P$1ZGiw|-FJ6~Bzzx8dDr|Q zhMg;Nt1|#U6poqXrs}A zNx{6wx;i?#G&~fm&U;+Z9&J~Vs_}?->9Up>-F~djUuv>yShy!6E|J^q+E}3xnbA?~Te$GD0%43got@7;|+t z{Rzf~>|6;c%G3OY*Oq=;(Umh3q&k)+HH2$U%v&|bMPxVUlC*SthbTvC4*YNylM(Ni z2ODUkDCCu-y$$VXWHX8b5?m^3$A1`LdZ&x7?Hdf){c}FxFm>X1qxu}aCG&f7UK&5c z9Z7f3u4ls%R#}a2h?G=(Rhtvf1Y{_pnvP`@57`$Tl?+XarM|e|P<8st=tgy8fq(*w zX=PFYoz`g#OWu;4&%+y*CDM*F>DTrp_VGL|<|dliEyvFfP#9|`Xxw+3`9()po|aTx ztq*t+4F0k`3Fan6H5Yr@Qb;0BelG{*(4Rc>WWJfy>I%1)jxWuusBK|Y(`%T zl{0R&yAytiuCDBx6-DNqMLBOH^Qq%|OMUJFxo@Unc?nWP&sXNHE-KP^MyG*-@54D%;zSg>WwyUG9zK2gRK#^w*iwZT$>(O&HRQT>n)ZgTPuvtfP%xiDTYG`Iy_U#|)-yjfmh>~? zpHl$=yBQ6#;;zEQ2?m;BCzhS-8U|RZTA%A1=oedmbgWvqUTzZgGvSocK9ruS@w@Uz zk_B4o03P{ zTP5V)ii(Kz*S^tKKOm>3Oo^iE4Nfd`>xnszayJUCB^=6lty%iSn_qmBcqOQJip<8q zl23uMdimdaj_aUCo*QY@;8}NK6bCg8jZ%1`=kJp;yVDU{e!hNw1WhCPda~RNDN`y` zJLOB3nfEa=qIw{nX(k`WnsLH^-GjKfITB~jbRylRq^g679dq8Hc#js$XInUY+*7;dUMqYr$h@Lb z6Lm-9#In^ygqmy*d+6JYJNzaXl#(cIBt)`OBBHt5@ob!YvE+OP=>!li411-EFAi(8)74Nb=vyYG`bXN=R^U z3WapEszkMi<=4sOyfrqd|SZ!g$PkeuM znjkJAq5tYQZ9D$Q3F*M_0z(IH-ye+o3TbJpM@5IjeiI+)F_b6?`j>>0WlrOHf^|Fh z!Y{D0<|#y6XYf)EX5?Qk_w)0^)p)tS-G{7FPJI-rtF0Y*aCpeT%$)U-ipI7hn!k&O zHSp2CW5)}Y9bTNxVQsBtQr{k>dSOCGhnAtEY8Y8vWVS&1yYxzp1=bHr4mUSuq>*8dvecg7f-y+vUN+IJtnjhap$8#2kh*;XBigBstn_OK;q~ zS=fKLlb)c%NkjDV@=}!btLi^YeeUH|NjK7+sM2TSPc!o)t4TR`a3@A{`T9X5>&txx#5~ZeFC{ZoPKuWo_iv@=)qjOQ@1mz&)E6jjqPVUk=MscbA;7hh-tG zGMG=F{>s%aym#*0x$rJIX4#zyDk7m;Ē=bJ*ra^DL@-kUcyaM^l-?sD5WhvXst z5yt0oo__-KobQpjha+wA+0p37FSkz8Uw_`%7*O@~TI4%L+O*^J31h@Tt_-_n8M3=K zHU?9M{?|0qqqfmO<&wB=RqmhsUN*Yghq;G#jL!q+63`n%8yc$F=l)!Xs`Yc*`(b3f z_jxI=Qr9G#;jnV0+U4D8%&t*m(8oy4pIH)u_=RtmFdE4+GBUMytlx)*>UnxrXxSc! z&lbe=ngtxnROw*-51*2)N^Sbavu{eAT1LX3!)CC+e5=6!W%JV>i%^;C3WlY&QJR{X z0+o)anLxcKJ^PWFW{<|hC{Uws;Q4c@ekmBwgZC7My!-eO2i1#HA;>%v(sH*fNybMP zd$1PFv9Pzq5-PAc-*Z36oSTd5(aoDTIeAzDTZ+B)v8cfCzYAhCRlCN`vMenxILY>Z zV7_I!kF^UDw;wF1SHSpeANid(Fwbsz{pQUO_WkbM5$eXLnhoK_a?cej6bk=@&WGm> z{FBoPJP(MRwY9YyZsgCOFLowK@;y zSJOx>=&8`&mf+JR@O6(@gP9_$0xl5uk0PSB=GrKUed#J%wO36D>gU?;DH?_t(j7gV zD_Z+9yyBBDmwQAQ^RP|g47=o`El20uGg)?6Sh$vJyc3yBmJxZhI=*t_*HJyy+4+wz zs1kJ9CwM-5IJ59|l_b!3fsXa|-$rS_RSttMlhggpiZkx`JbCiu<uSBt{6*}Hu{y(v-?s@)oYhlKaWbK+>_t#Mg+1cp#$^6K_%A>-Re_E&_O8B{4E ze`JzHTx|ok3Byen%vn%>#3`%km1BjQ{r9KaBC6Kfd2zKq=#N(L6Kn}xwbi&JOM;=?(W`U@={&CDbuIbC9AUT`L? zx3)gsC!V&ek#;dB?a6J^sIS(nE7MRPkMs|k>VKQxZ*}erOvgh)ZwU(5Rlj7yQkF{b zrdrC%AE2`6sUSmj_o<_7C=!^0!9bP;X64JSlY;(H$vs=qn%h&o*-8@w6&hsoPhD zZ%Ow3Nq&UY(ZozC*)YxS_Bd>8v_5AJ(s+oYKqwQiIXiB|VS{Z8&Fcx?dOYs~DNg-O5E>;=%llALvmBN}>fm(Tbl1-Ma z&VgW6etG%V@8S;@-kx>py|rC_J%B9GgDKkZ4q2C?n9{CtybsmdhhmRyG_TF=ci(-7KgL1om#$i zMB;VxB>u&p=${}}&R@ZTdLnSlgGtJ5{*$Jb7N^41t5-2*SpTp4x)^N*GymN*jCNKE zzfoBxG}=?Nv^r1;;d{6Q1qEyON!7MPMQPdX;%~e+7qiroWJv<4qdug{Im?Sd=g*%X zHBpSM+8VaAx)^k(k$ZK0vF8z6>9%<~L;S&Ejk`}$kfHEl73Ii}G50su8iUb2f1v{R z;g08%*72#S!HI&y=_;3r!=*~84?k)mVNDv9eDn6LU402NkuU6JwDz8`c(V{;M506-3EvE9+ZE|ZM)%2;jHLVCP*sjUtKrjALq2d+Je z*F>v8VR;;D7Amv#88+_kfB^D6IVuQGQNKMm_k~{h$;nCBD;!SGEVnuMw5gR*INcSA zACDs#}bKlAH| z6SnrwK8zAL;Hpe~h^5}^o#Lf-cmb7IVR@*--jM9XeI$ed^x}{ZCXSR>7rA|o#M$bM z*ZDC3XnVxwr!8tX&_9rCRO-3w$EZ#Mt?QhN#<~)Zw;U&f`Tnu{~`_*bB(+8 zus;gPgiXR2R~G&Fz?_QF$+lge?Tiz#3HRTfVe;FZi4oXa2n=U0sQ!h0G)VCo4^;(M zWToT>H?@^b|MvCk`Lr0*UIc2VtHf7T-usL@I@~X{XrZfxI(JFj@e7Adf2Kxzd;5)z z&(Nt@;K3>i3x-04sL_`N9 znnGD+L>z`iKC;P;`JZbDl{?aJYioP|?p@DJjG2JKB}tcWIl9@5eEP}XzH#VeX{P-Q z5K9*Me17U)M~q-hr*%Z9HLqJFbu%n_oLfrD@aX8scC4mSmVDrfj?ot2EhBCf0JxwVl+GUFb2ix2L}fi7#PxM*nHK8hM3I6`kqRXd-l4s z4VA)^l9IGSUs%HZF8=!Zs-dCb!e4(SmpP1h_pd`#VNTJ}3AkW0Gc$e7O~TJKs*{fn zqoShHVks#zU)ReHa5+Buj*S`GFoSp@wrABvVMEq@TH-**Z#eB?OF+X#nf*Tr{_H#_DH~c zIfO-;LqX5f%c^cGPSg$qnX^UY#)h!T<-uQhBqa?X*t7v=KtaFBZ&Ibr%*@P0>jh+i z##=g;jm7MI+tyE4F|ewzP&8EE8#it!MqF3XgqUZzbjdKK3V@vZCUHcsF^E>n!=p@g zfB6>eC6UKwew$Iy1k;0A?Sk+QHVL=AG{d!_N%@RG)5?bOf;^45+g?Sm$t5;@*J}^A zM!a_>-xAqZ17pt(PQG|*VNq&7Bn;r=PPoX^rW2wL!`k%MmH#e~Hh_V|℘dCl#qO zlCHCZ8NnB3qx4L92EtR_mj*h~_l7>-wAg?;$>3#-&oe27%)D1VH#4(vw7;TfY5C2A zG#V>-aNu*SFAOHA$5L{X+fE2H^FQzm^Q$kGS#6@1@>=^=W%N+w|kdjlch{_Ye5_ z`6dFWth>G#SUy2-FO`*aeUVJ9be+Sv&UNjYR@ZuO_MR53Nsck~R^ZjmDJYlQ?AMAD zwo;76QNgIj!kPhfV>j>U65K8fRa)@0`k*~AocmtY+a>J1G4H$0<~@J4GxpXaSj?t8 z)qVnc%piyN&(SUY$GtnNQ!I(7Jtz52aYCU5JXEP~016l`X7P1V{{tw3t8LZoSU^C) zzTPr-5+rQ2pjjT&uFCpyzTW1f`<0cIizDUvlvGqmxrFUik@4B;htYSryID={P`jMq z5f5-O^EmA#Zca{lxEIa+EuyG-!!hK&<)bdax_QPnY;8$W?wCZcS?08i3^TX5pK@ke z5Z&!CG0t@*Y(gVq+;y$qcQ?AEo2`R{5@63U-}O$)ktJMqhJZ=c{D6^@4%7kNTtg^r z-U=`WUAkqh9E+otIF0O(d1DZ@tk7j~M?M8$71#@;HK0PR5uAP{zvN?73R7|z$TuUW z;F-CVp_0OUyRIZl8b8{ZI;)@w07KF3;LV-i^RKZ#fBvL=Nj=lC(3cSv7iX9<*WH_@ zRO-H@>p-Z|`FL4;V7Sx{SLym}lSD9D8pzdp@+8?~Wi0p4A7^=ad4=#X|9uA*Szmp* z!;Oe*?ChJ(OlFzrm9a}wZkf@12G^k76USDE)kGfh1~yH znT<~aIR>fJry2iiit*bTdWO_eNC=&34+n+bKng0$QbS~iX<4MSVT2@cU*;C0ln#8# zOPX2g>+OwgS6(1UkxzP4DP! zAw^dQ#pXEewh&f@1a#*1I<$n{4KPechs05qB8=R^`4loP;J+ zqwy_l0XbjqzgN`Vhe6^JvTJR1wFjwtYO=fET8Cve z-KBwaL;g(=dr$eV6M*J;gqEvfB}P%(j$qmweme$A|NsZ~UMLei3FWgJK*0 z4VD`-|7xe9C>wBU2UAxl6hR<3Y^PhpVn(@cKLSv8@7}!B+-m9-npZ-TMb1V1M#ms@J>8N{j{6`J|G3yd!=^UjeNM>(b1vQyPI`7 z$o%<4{Y&|Q6WvyY`uzZt_>C*nVWW73LJyyIe+9N?{-gNVui|dmY$#pBBm0|689pnu zdVuotARMBdO3ELFvKkgXedCw7M-AKGqi5!q21Ubm=g*{kp7rmU8O}zb$e5UCPQk$Y zt<7`t@Ei~Tny;9G2 zVp%8`h8(2`F0}y&yS0z=K;r7NKm72Ee6%fx%hb3J`P~P#`n}Bi*T1C!k0Ut7wAR}O zes5Ue{ME6VSVrmkfZR!NHRopxMlzNFg$C&50RnnF7p5>kerj0qQT0HciIF8+Uxu0< z63DQ-w9LW}s?jS@iE7i3JBRMg>;6%w9H~d+zUv>WJcyVu_D2vf)>1EizdqU@C*Qb! zJ^hyz)BeUns{i5M!mG2gzrMesK|tv4?k>{)4J%#Js@xV*XfKP+4?Ro5t#``_DpbLq zmN-w|li6P`9xii;_ug908XYy}H>)#7`i_*xa^~*(TsFjH)m);_sodT4c*kDYIfVWp zc#Jpz*MdhPqQLw(v+dL>Ujm~mz)?TKVDh@5Z7PvVyYy7><^{TX7zV-)nw4+Q4_<;VgSp?oy1AJNU5o<6}A3(Q6)*1 zK(J!ypcNlYhC6@0Hr?h8kj1Lh5C@?S&%v)*stzV0r1FSDnULA|bsrgZar31Y~SX8#ZkMr zm>u4w)?E45;5Kl4%ggo&vVOypY&Y5bcfQILpkdO97jx)!?Uu;^FvATr9{llMZDtbn zKiCmIdWV`5vTrtsyjXF}D;wXow>A3DWm{Zi_eim^nL71`e9WULmx<>ux3V+FveO%V zD!*QHMaIRbh=^3EteSvB0YXXBvQhveTTLQD8CtrClwE=nS<;Q;q+`yQ8`u0(JL>q(UIa{w*K6Lcsi=%!31 zc`x$75V{1##1$+Js4p#%7f}oY+4*%Cm=zRmtSLsmVFu}8Z)1U7VT(Z)g>t?wYl70& zhv|&poedgsSNYm86sqe7eD42b9hgzO$|P1a>#U=MYJS#PEM+hFd(TJp5!eVKrR$pc4r4N9cUl63Va}?{X@=1oBP=aupcl4j*pC>=8e9vpAlm{d7amgLpz( z>nRE~qgfj-J^$dRDv6;jUP3xza z5U0^BtgeH`kG5NHNTwN>rExAWUj({jIj(H&?Pi^|1ZoY!&2I^n>kyePcSvZJ`F`ZmhZ8q5v3 zS27ogahS1A{CrWOUb`JpyhvcDpOA*wNf?6BoN~A&*N=;vl@G%$!MPBhm{s%BUR>{Z zy$O8h+R0NA0rG8oyC6X7xbZz zjEmNqubX;ys1`m=bjXx%ykQ$4?@sV?y>HTosXEi>tkZ@He0r^bD)4QQp~D_XLY)RR zdc0z+(Ky@cZ&R+W4#SlzA3R9Xsc3SwT5g;+4oKKOhuX{Bnc-^8I<%+??Vsi}sIwc5 zW%RZ;CtNpc0zO9Plb6C9tJtw5xzzn6vO=l1FGf#pIIZS(HvJ81R3sxt)ttJOBD)ySoJ~N?ik@tiCDQ-}N#3_2$+yw=@x%k{~Ym zgNLhv67@dRiiM*7TiJ@jF9&+@=o^1u4!Xd&n3T_dQ?j@z0QHl+7I?{&F(u>S!b|};MMXsyn3ytv|IR|1 zD6}^B*`LhKEiFpn0JycW{(j%3!0tAWifkom=zQK_iPY^yd4ZS0rQI_ZstIR8qg)Hl`1$^1PB=? zw@V_+#i!&@>bgK+m;Fykxd!YaLB~*{W*F~ZQw5m% zKcna>&tk`$c=*{{2v)|z>CLD+L)QwVdtlOtijKan;C-;#BI@~P;@&nb9o+yha%-hG z8|bv{;{#-7NaqzT*PT|2Vkb3oQ0l5TIe#epKdZRwz9aPK&zG1t9*4%MLIyy(0W2Oj zK&9{<=VR?(&9*o|NIkFcxV=9tVgWB^$$f+WvRyZRY%0fdDdAkZCMMKG>Hl zSrJiGHwP_3CG#Jn(xYOdvQ22ZE2=-7AKCH&{{yQ11SlY2QGr?A3M*&X`spc4OytJ4 z&KZ_YGx0O`VI&yP(KAVdX;CxCeA>wG#}LpC=joN?8s_3Xbfd1609C`YyRo3T9hx#T zGb3d$=Od<)vCHzgH^fh&y|Z>l{8`Y|H*faL+>!!LXD{pJlm#m&cWG;wEgn`73#7_V z)$FLoaEoUB;jO*?A4#?^E0hLBo{dS&L(vc=D=QWE@N+f*cln}=VjUa@KO1S-B7on| zTWO{}3PLmX>0tw_|BiVr@Hye%#K(QLV4P_ymjNvYrs}PIpcw!K5;&qdBR|7@kPW2!`93+}XVv~>8Bzoi4=y-nB{cZJD?8|;1EyLHxR~^!-&YHT# zfMDUiw{I?RW8P`fgo)>B*1yhQYUHKX>a^I(|GN%rp_;2+)w!YvXo@==rlyk!z3xsL&+xJkI%cv^>l2h51YU1jdvq@j)7dHPRo+SLu zHMf#)1)}%o2k^O_IdoLz}>B@&p#u1w3f@&5^rZA>4Xq6tzqn<&J%ZVxkh@RV^oKSha+Y$ zt&jpLXEr%AL6e@$_thQTg=L*5m;b98y7Z}t+kis1wU0=rSpV9w0J0yj>dN2=(*jNe zn5z_Ek#9ishhG4#GP1Or!c$g_wsunIK&SfndK*P@&R>HG&wn?)L&*R7msAYr&Ru0= zV?#VdFry-(U5V4UEYQN-<8I6k@f60tt`6TKd#?OtVgmLcq{f#^EcCY_b1i(4saq1z zm^lCVdb){!Ug94ugxSdPigp&~O?a&YUMC4qpxQ+r4iW;-d7wg$>8$mb_+757{-C5; zZvDw|S$l+Fg}PJu@Co`Ru>a%!`DR z!kQ#78Gc(RGpnX%hnpg>s44Oy6G9yPAB8+tp!J|9(v=^&61#!#xFI0WmE}(99BRFJ z-`%A8aqhTx10n4~TQ^>5d&6m&B#cTPwa{;BK9UTOk&=p|FGXDtay9nx@`)$cn)g!W z>d5fb|7WR>o}derVzH+ArZPkJW!K0ztLP#HbK_nw#D>kvVrSYrvTa8H@Q#`s2Ota* zaXZS{ zrxg{gD~_r?bxCii7P#?C_fmJLV`|4`Z<3AyMSY*pZA#R297f@DwDQVz!Tj*YA@mQETe%x~@ zmn*+#IYX-TsktQ!Y83I@v8E6hCWq@Pus>+A4V;s^ptx?16d8$lKXy(3jtabtV69ij zc6^qiT1FN!LiaLtB8m*Q3T>_AYB#4v_eh7Ab+8_fV{acvCGY4DoI~CIh0Dd*wS*tN zAM#gpWn94(IZZ`I*zxNm5%W*r5k#7tsB!Ag+!O`s=Rasm)E{+lkhqSVaCf}?HR2?5 z9C?Pq4@>;!KNOMNry!qSN?Msqo4>Au%agAlEss2Qh}Uzyi$et}$v>e9boo2QkW=MK zk0Sk}lJHLJz8S@_%6Ln7k*3W>TLVvOl1kxa(O(=`0@M>v(=}(k&=I zbc#0CAdmkW<@%cIfplx<1RB*H7Gb*6oCMZg6aABcr?Lx{f6onX{|%l|C`~VX77d16 zTuh9VsArfKchH|+2)8vyp*Yb3W2@`9-*fCMC^;rQs^n`$2|UxT#RWN94-v2Drzb5$ zNy(zavX0VHZ6FP#yowym2v)^o99dNbV2}XFMM4d7-4~a2-{Q(~KV;&L- zJq2|a{+e)(7oB~^q^EUL-lM9)e1GUZm;3Ct1fG#9@{XxTNJ!3MQlmpyf?-9)B@{!L z!-_1AWb>q$k`lh|EC@w#m-!z&99SHS5C25uNc+3LkY`W>QLrU?cJ9yy`gHZf*amx#w8{ky%Z}BVV*Cv<4=8CR;?=!EiFeSNr z>BoSz_I^T&5<@h&7StCbz3tw$1>0wynxK zp;{@cK=T$%kXRk&p}G96#O?fJE1Lo9)>lgD!zNTsgZg6&U_!XYJ}ooDT>i%WT|FIB zQGx`|lA2t{USIc{M6ZuU7Nx^(YdLOz6O6{`s>H_2f>s7sKjcw;FPyTnKIgw{jgFqjA;C>Py7ffrQzJXcG3C z!tkH`t&k2g4Ry{Iy@`A(P|H zr?0n2geB5>ILdST{7V+q)x+)aY%oiBA8z8KK7Y;xIUy4W-tcc;>$6#)6z&0sN-*Yx z+nu{w(8$Bb7z~2qx6XL+CBQR?FFBguC>^ZFwhgaOf^VVM1Wib5YI=Z08s-pYaNiPBg4Z}K(VRJx(A!t{Q%Cdhfr2e$Dwq68@05{qA7E!-INpB-?!ZvtbfzLf~bj&+`mNf^c-@BmB z^t@-_ot~PSA{D`fZ_~j(fpr|wGHqZGu^q^X1q~t_tTNTreDH=)P{6c+ocwt4<~_Jm z4RF3~5X;9g;C_T?0~*u<>Y+BGynt2Z8yF3rAMR};@>R7G+|i%sP;om!4HkXYbp`Q8 zgDp)jLCSs91dfCSwyc9=oSO|%{XqLv3SY$M=L0?deG>dl?!C3!V@vJ5$Qm!f8ehQh zzE1?-NOM!ug(^Xq_@L1uj{t7j4#Xak0I+4rqvmwd%9HrofcZP48n9|)Tt+>)0e%Q# zQL$^uQ-T^;(X*;bZRZCrS7W^OGpjpp;O%v@e;*BIBpCT?KRr0Q2*A`(GS*Gg!Xn@DTar%aACr+N%6clu)V##8w1Od-T|}Oii-cD zE7TgYruO_=7u74@>%E*|UW)kQJNLK=4BT(~( zv9Ym;M{DZGN8H!@G>EwY_P_<5#TlKDkZQByYkPLtWVlp&#Zj-i60?R_`W6Y*<^EtH zQj*FuDtAOw3&b2D_J#r#7<{cDz8{oKAoD%p;#IAF8L9~w7{98LZ_MrccjOz^z6j=M zYiTi}C$V9Y>rvu*I!9L|T^>Mm_Jf)P(lmpf@mC0#m%r!${0MRWc>N=k&B?6K7_p$Z zxs?DDlAfIWH@KZ`K*=4(24JDp3E#R!(06FFdNw!zmLEI3V%AqBXH+ zF9A89;L^cw2nRF{4n7whLk04l8&t6-jaKe9c;}neZh*Nj8K(CK;*9OKeQjM&{*kAK&X+H zlR6YA^*ZfSe`1=2+P^uaH+=?msr~)h3cUeG>ui4D-7nHNF5a^?{2{60wQdn} zo6(qEP+ZSR&F9|2Gp&SPv(45Hk%P$$%gCO~5VMJk3vWs4>S;x>JoQq`*Vn+jH`M42 z8qfyVJgWI}QhnbOiu-K9s`FdzO3!LzfK;v=96A)~O@5bST088&5BjTyvva}a?7LnC z>vJm!V{;)%0nXk@5WLREJm`19(LpTFhtm(@y8o>G?q5w+UAcAipiRvbigQee4K8E(YgKxnmw{!f^D0g|0SO8@^us?y{v+ z19SH>?%po1bVucQ z+_XNeEagZ=;SASdv)1JmlztAg^DWS4^k99EGJ>d5-F_`7t{lBJ6 zkt7clHR3P=n`2)Xdzz74El`axi%$Myib7Rf%x?Pp6Dn9X7+0QywIci*xcCqwCTPC8 zpb9fwxbO|kpU<~RgyC|hBuJj?Dsj`ZnA0(k?rdcTw%7obrP%pI?6F-{*`bZ%WdkTxarzsAMm1VY~szb!JB2J4Ry8RBBY8wqzaOL zh;&))LDYoa1159rG(`qc(7us6!CyX_0w1ZY)_x1MRfL$JMtoVTR2{uYAlgDbDNK-f zaSwrRoMzqF6KN3^xJ{sOTz z9n@Gou=Hqw;{XZ{G$y!ZUbuwf;r@0^etv$n5;ZEhfxRGAc?JZk-Tk{(me!bc9e#od z-Kntnc-}^#X<+eySVYxh|N5`9y;BVe5XS$b$Y$ay90(C;6aqgiIJ+lFE&`YU@)4^R zFA8Nn2Pa@r7tA2*5v})4P*7EMWU%*gVT%ZGAhkQcgGmCAt)U?Q-F^&a9Y^e;2OzI+ zc_!70;j>){*{@$86W61H)@`A929=?priMnJM1fLx%p>`VKG+37e$NgJL?O0Su(tL< zL0%pKd&hn7$^vqkktTr~DctZd`q;5dJAB?SILqrS z-kxR4gF1Z(XLXQ%@z1MQ;MguGBAZj7GM}7S4t(o_5aV#j0t4QqI7!!ZR=*7mNDA~~ zZ(1;%VB%DOCs7WPXeyX%Mp>uP#Njlsf!TqP4z7o;lVt)w2x1!qPs;$k+U>9K%cZKN zJj4yLMvRG%&%W$faTh!Scp&i*BTLovGZ>MtL7$%Z4lYDN#VD>jG zcg#inbtB`z2$_KQ6v1_MdyBjTcf84a$Ogo%vElP);w}pmKzIh>8~~G)z(9cqPv06b z3Cxj*%@OLn)jrThi_0o2e&`MRiO};IufKn ztu~NGllsA|b>aMZ&E#?Lu#PlVaR7g;3SUvKmav-vCe8wTe+d-o?|0`s>Qj+rfa4s3 z)1`vY(!w=<2yWr$lMQbGB1IvGen^CB>Dyfz0f?0gJosy+UOqlZvT^3HqEHuTUb6go z0xJV64BQ8DkfT5b4lj)#0@6eX^zf2r-K++=x(0lltddpH+%d87JAbRYdi~0{dcWYzDQ^wQP`&@mpeE(NO>p#ju2q! zipR&R+_S;1m~z>vrf}IGH1UbQ*p~xo?7;p8$0$>{5a`68Qv98ql?JI`Mc$!rNhI7D4nFXlD zT41{u0i^0{egM{!NI2_bmtUI>3qc;PQ!O0BWoI`bQ^BRbGnWMfgwoYVLJi1+wqysT z2prS8;9cosV;2xmg>fSea24E$(Qp5*M44<@AiPfvBrWQ^a2OBXhf2a$NV-h_3x-Xk zc%x%nG!XAP{0K*nU=jl`*aG=KaTS17K3K&~G{4VW>YRoCU)~)h?vsniqYxT@!8oD^ z4TE6vu&~5_sFwiain^oF+yDAs5+J(v|FW~4|6h`t2gx*pUG7F+2_Tij9|d{U2YL6N H{PVv7*kZu$dq-*F_F-WDmJEc3+0|-bn)KJo0 zLk@M{?fJ!B_n&+Jyz8#ZIxal}?AiPK-gw^UdA|9qDF1|vgq8$_LXk;7l~6*VPFtc- z1V(4iz$=33BZ=_WBebLjTG`47?fBC6HA?O!+S=R-ZEj+4&Ed7Jor#s@eXfUG4{l$3 zgGO81@pE%q{I4J2va&ViW@Nr;4;MLS{Z!Kqg`&QM{2?$-=z+IVBb1hStl|{AGV17d z8FPHHv71iOLs}c5*m(AA&vQl5f8Smf(2-m=IAF6?^>y0zefxuwRO6p--4fpjdLH{; zmgIjr|0N!^Pi_1{i?#EV41#e@nNKxqYDcyInJ_97 z%;|_jj}f!02ZctEyQ3({k4{n24?IB$5`8B-Z`k=Ow5R8lh^u^S>%FSjwrC+{)v|IH z0SlbVsrgA1y z3$_Y7yFZYf-az|&bCXA1#h?)DoPpXc%t&k7S&|wWu=wKMEZ@>3BBFA;Qmr~LZP=b^ zD4)UvO@KmieS4Zsdjk{jEmJL0lAw+%MON(5H|&ZG|1EmyvZbfKx^uSv`DcH1c8)(O z72s2@bv?*zDpha6b`H!3pS7G|DARR)bzpnpB^i5e$+PM*#^Q_pD3szUYOz=~p4!Xw z5?*$Xz74J5aqo3KnJ@J>*zN5fWOm36%r9>IoMxnYr9pq1kJYhE#Ic2chm1`CJFkQi zPklXHY91t~=`5vw3gu3AQ8Bi3;|pABqn{$4t?0GT=pAGKmw(7| zWRi3)4ksqxEFnN8Zf%@E&Z*c!ZY61RS5`?4JSI*vb}>kQ>^S;~7}6skfjUT)ZJ&L) zx@MEX?T1NBX9-)8O5WtGFPG<#$sD=}Ll_);i!UtuL(Z7oK&IvR1rjqQZIA2To0}UB zR$nQ9MWt*+53D}Vuqd~R&Yl?FB2W?BB8~JOHBy#$`OxuiT+g31K6cA)m$*l6!wYp; zQA$n!ekQR{>E)T5#b=JtyYIN0jC0FlZwrrQcbA4OZZ9R$>T0!!+|UrV)VaV=(GVYV z%S^(%6F%sU6la!f{Tiqd;cR*i%8kmWCOZ|Z0gmn=vx`{ zU2jX}cZ0OIa`N3=ykc@BOa^Un=;`JFd29{gVV~U&tLoZd4_tsq)6IQanORAg53PDc zSCy@&RMZpiEZL?O*Q0rh@Tt-+gX-c~3ak>=f@ibi3YRf#mG*8F{9&b4~1`){>z#n=Xg&xMd89c)1_RqxkZ z7OML(jKQ(Boigb`;9*6}fnl90&eaX>guljR&!W1Ue!(&y3Sg&l!KeC&xj;(WymQKrTpx@r>}U;? z8*$M&oi30c$drHIXoA(`J7F*gq^lSkvg;@(CT_WOadk`!ML3;G*+b0a!)HIPp#kkv zbtd}xCfNm*)hPc{7DHJ+CviT`uc&Q>E{~-K#&CF}J@`rEI0h5gQ(L`@G-8SG?e}G+ zeUqV*CQ_Gblk5D_ZW=x)d4{!y!FfW)WjxXsD~?_vB$jQcd+LUlob+ z`u-#ZTc&N{f#%z{)7lbTA+Ef#$4QNh&6H5$T!;u;aK2A(X2`{)U%V;BdjI|j_g-k| z<+Rk)H)~_HTBW98WA3}Ua*=l)cM9#sl9F=8t&Uck!MZH==f$Yy>zJS)S>krER%VBL z8!~cooLy9mj0JmpPIAT~e`t8v?cAiO6?5X4$HMJzVJMqF3kLC$Q zA8(O{5_2=o4FvA|$mb!GV6;yD!Wf$NCBe*jZy_V0uiQ$1$lND)>HOtJtgN2oXyjCr zzSL>HJ0l~Nc06u-*3wE667LzDr!S7W;o7q`i_;H}j_eQjY%GRLr2VM5`xQivURjq? zs_W^+uU5`6xF2l~1gC!a@{iC~mw44i6CEb;F(DBV5u32Eo_4t<=5QZZ`r`YkJgrh0 zk&{E*jOR-wsV#y|QAQL=vVLN63V9@UZ)56p@sLWI3?;JJg@uJAXCKx-m3VPCg#dLQ z;_8o{(;p&fr8jRfd9B8B&ya0$pU_ZJrg0S1D3J^ACCVoV=RstY_hD*XcX5V~uP+>S zF8q*`p6iI?@jP~ueDY)td+WvxF}WCCjIQHonr^kj!gpfE2gxcy;*9I(w+Iri;fdJ! z@m`e{*fP0b^jw@{wsB|t8Fj)?258mx6Ac9LjQYW;kKdi4boz-`P|Zn9Iy-$3CP9#^i+ z%{?U{CB0wvPolrZg{vQYgHBwd8YU(tWIc}Fkqfi>H6~MWYCW_#*=*x}U8wP@P@}KT z-8JLjSq!fnm7k=ru&|7Rf}^>9rES_&DRt-gU8CPLnzyNt1r9DT#jrPfP}h;fh;BD{ z6T0jS8QWu2)!76EbqELuvegUX^Q%|+ksEFfr03W?DAmeyIXT+3Z|50cg{<=S_BI_V zN^1(B%dnekwMwqz8*}=(;j}s3!dzF{H!z?MX%{%h8zZ;WpVyoDJf$y5oLEv)vIV~L z^5sj@(W+9Zz^hplx8yH7Kg+>n2Z_5nvZiF>nF3-Q8XF=L6jX!>03^dHt8%t@6bf6UkM!gi)Q6 z^`eqb81H2G73g!(2soJ6ErtGIxp(HbuYl!?%uk=rHc!p8MYNfgwjNvxSE)wc$Q_Sf z87`I0kdIR;HN|X}BC8tMtX*dQTuUojOk6x}wYSVd+volJ&{-^N_^9Oo1S5~b^57}Y zy$g)Oxj#RWn>G4U;-`W{wuX!o9tlZFNqunHS=M}UX)sQ}(R6FRTdT&YP%Td@O(u*) zzcll~y?ghzj&{dogBLM58FZEvNqx=utzHEl^TBVYu)RG!B^Fp-B+eM!*1cyt6AG5{ ztMeN?AXb&3IFvf947*|N?Cg+&S$~X|MarRpLGqaMT->RG@e-2|{O(%8RCCZAHa;>k zGfg(abfG5=KOLg5wNgILE;u42JJ3r!PpH(mO1S=Pi&O=@MSxoUm!9vAN(hUbO0|Q9 zbkEKT*8X6ne5riggJWZf`*^CCN-378sc1uXdmyC}0 zVYuA#aH(0H0ykG!eSQ6z^E8P_6e01Z?|GC4X@0anmw=ymM~)<1Ny!7Us}B`36cV`B z@qFWMy*AJ9K?Tdxuh(l1VpO-aE!tTf&3R-u`)0l?DO;y_c`qjC6w)X6I#ci59=hr{q z=Z_1``kgD7(zfZeMSxrEN%Na>nSM*H?{;_Gz7>+=eC|IL=n)%2aY#(l#6MnW z86Veo9Ct4l-k+x6)UVTl$1OHYfRfsKS0ydgO6%zGaC&ZTu7cwS7OVaE@nfIWdWf`I zXsW>-CofO_d}Q1_`g5L~T>ae{vIj33P%|@>9@i=1+RU=y(>eE{Lg3~))nj-qO56_| zsQn6WS`Z#1Ixyyrp1Zx?~omN z;IcLEcEG7uqY;d5<1C;5NiOnxfQo@3KQfYrjOI?mLgmK`^x4J6o!PL|T6J#DbqS9( zG$N&f81f+O7nTeXN zqKc==@1J*s&^P<@!zWw6zRpk^Qp@9T;|a1#RSW4cDy62Xqn;;6{4QH>ya)81C>%Gw zeDOR+2|Q45o%=zF(}sc2;rdUg`>*Sqh40fv1G*W2RodFxIL1ebsMqYEZ&bMH`IClo>9R{NcBgz??8{PKoZ4FG&FCs)k&AxF zkic)B-8X0bF*oIu}2tHqlq z0M8(TD_1L~0^M-#q3BP)()jIW{S9w#s;cX%rW<0Q0Cbi4so$;mww#nBAtlAnkddAq zzMw&T;i@7Odi(W>?*_Z}4*7+K&885JGSbrBU#^R9{kj>HY1kYH%NSDSu#(NCTlH&T zpuAyeXL;zRw9M0|tQ)Oz=gzIxzepp$93X7Hzf0hB0q;|&)LXp$Tbpisq5MGC!#oF* z@7y+7@?!s`5MOFqlkl(>!Ew#&y<2z`%5gG~E>aDaAF1iJfYydzVDRRwAvm(3fU&W$wcOPl`StZC{lSTH9KSu% z35C`h2$AbUR}JUXPKVXVh9%Bc%Zo-pNNnt3!o%>{x9kD})hgo|9Nom5kOnovC|t5W z&0I6j6HL*`q3t;fE~Db1?b!?gG$zribR%eQQeW|V7cr`W;yan3SQxwNR>H|)^TuI9 z&ZB$OGuiM8&<_IdkDL_t61?1)E9!hOb@_ma>g}V(@Fi|Ve(_Q3i%u&nh z38WWbl2%TC)?K|;mjh!1C0N7u1CQ zTFtJWp9}3SfI&#ham?AcZa`=6au&eIBJ2wD@jVK>m(72GeYbdM*<~j$plYL#nV(^8 zSb)^8XihPjf2>@?s&nv9nVriiWbqAQZuzey@sC$=VLI7<^(UTs z4|SG*G|Jkzxw)aYG3ra7Z^d`PEMUJpcz4wLCs}anK5lLa1`fw%*Nr%KX8K(V0Pu!g z#9nXbsaL}yjC%=F2Hs50dD7Z?#I)znjg6&xDjWy!t)6`RuhvZ5<(RsGLMgCa3GLmvbuF3)!A znUPUC#NV9L7{s*O3V?Zs^V^Yv&P7T}N|#~s0Jt}UUHd`TcaDMu=DYjjWwMl%l+4on zHj@%x4}^}kdW$q}x3;%q+;>OSrx~|N+a5*BLGycH@b6ja9;mu~)s9xoEG$&}(J-DXq(m@!@NMU5gOL&)-#1=agk?^AQ$w9%^d@Q=VqA z6yU)=Qtp~Wn70-N3k{JP30wk%sQyBrz^bAj73Wn)^wYU_AG*2}rF%YIy!pU+^Dq27 zckUb>8Fllt`8ai}R5-OtSW7;@P&JRNbSRrLJSt0^u)7vCWjwA^WE-Hzd{#z$FcT(M zQyHqBeNBf!;XHgt9a0AwFUu^ji!%}W{j=>cVSu|KrD1wex0~z0tpE85x5o(^?KrdU zS$GYPa!*W#s?3X*+{ZhQ-mTSb`t$tjO9B}Ud&jzhU*Ej$(c@0!Msh`Vm?IWwB1n<6 zeqkX(!+!0W=<)G!Yez>vAjCM29_vEDrKZ43$YYV2trowS6FAqQ)?zY{9|tuD4Ks+e zQn}^m^o(lN;pUj@8ivlQCJ71xGKag&Cw;sMAlu&Vyz1A~0X}szHb~j=7y82J;pD|$jbJ{Sk+}A@Um+_2R1Y?El*rroX2AL z!6@{9q{~V9)BM>BU3?3{*9xdqz;PArB3M0&485}lo(ISP7cJsh2fP8!!V2a|WIBR} zL2x3ih(}FR5TpLQ&Eer8iG>|AUesgD+joF?VrZUv=GMxyzz+eM+J}K>ezg~y%**DK z+U&G%Zs!dq5cWbnCxmevI_phlCZ;zn!OU8!w|a-oxcq z1yHHA2XyS@lA2)H2U7SSk@zrhdx@u7YI6USeYD*Sdg z^odZ2kL6`^pN}6oSXiW>&Gi81p!y=R1Lkqn40(E_>1qMk>*?*Kq^HlzDX7y565Rac zvb~sg*Rbib%Q2*z>Hg*nQY8UQCMsvhFDwrglaP_A78|uA>WS}$m zz|fIIL`wChEu2&4*)y0j)L*?yP1(`5tV|nG)5JsL|1T(oAt;?zR|5DQ@Uhqq$E>my z=^na-!~6(F_EFuK2nsxEy}UDsq37F5470SX09w5~lfAL7$*P+#IMvk*)7z}RVfFip zUtHFoKL-7b5v@DNTW`sD%zo`2Nw0FBElLPfAA6_p&;l35D;b{Mcp@g*5~p$WG5kD( z`ip8Ho5Ta%4Mim%+C9+24GS%ntnA1sQ|iPBTR$qsuJA`h-4fB~-rskj(+xD~;{I(m zGaFL1Z?}i`UZEfN zkhtqeS(CG+WaF++ZR7Lp z$6ogRnpFMSY9V{{mfEgP>qxDUj1WtCtC`%Z1Z3@az698AwhvLOQk(dpBf9!GfE zJ1@Kp&*@2LYn5i-rt*XTVE%pcq7jAC5uOaWw~gN#$Lys)+BCyuy)D?+Ikpg1o^iO} zxAZ5W#Jch2%*>-#e)wfOK{k$mQ*pd5$N zRu`ui%0hPfvP64!(x>enqL=+WH0=83@5z|YPQG)Q%F3a<^zXQ2yLc*KHp(9ZQ4Ic=&}_!D;04H6~XyI zrV;@e^hp6hK|#m}h{XB9TKxdj<_Eyq!81QH`s%qq6G30Vxnf&`LW$yEwm;u?ca#1s z9^;ocW-Buwy#B7IV>(p)wP?%hIgRa&x9(CAHZ5ZZF6EKD6sT76?*V?x*m8vv{6IaE zw4R<`w2*5V6x5W~Fr0$RWm?*7?)pO|C|0d4`v<4)^78h_&74A64-J+HJbxFj&6QKS zWp(vrFd~qloO*RxNQm=i9cWYdRW|hRJPd{WRHy^q;F40JB4(yDO>yC?PnY~{%K;W!`Ej|OeFKBEuSexmX+r%9qI&) zOXrb_upYWr!FCfG#9IBy*v29BZr1K4uJ?C?ar+iPmmy>UD?ko)efLtM_UJUA3n20q zD(B*RjT79{|NQ)zW53i7Q=@8BJ-KwzkhzQu`vK9r@uB-9=%P{6Qg)u}Nxz$$({M8K zYEQ3Qw6wGoITt2JaipcCQ4NQSGTxDpQnF35XpwGdB}pWAN*2tKW9%GX+MKEru#1rr z>H9I$%tMFwshpl3E}2s_{d%BrTj`aC=+}co<xyq6Y2gycccMJ`jlT}!&+KA6jHp|&=`lUIXOPycUX=T6BFA4_5f))z}<-N z?M(%hy6#$9j8r^_uy8ov%bt^($^>Fc+V5uPZ%6Jct z<5)5*fU@uYbLntWtc7PcC;5cBJFQ`-rbaMg>#k1K(>2jr%F$*M?U*u)&{t{XPD}~^ zWDQ9~Fz(cv*lmxs4OAUg7y;XGWvx{S=jb=WeDAjCxtvulT-J^o-$d|E&?;~Bpmc~r zb4KN%)re<`OO~&~cY&b<54=g9^TYh#&2#unw^}OuTGPGD{!Q_o$3%HflYdLFn7dn8 z*Bozoyq(F%)kP}R!!@FM$?=M$=z1R0J`EFQnq*9c?U#PNsl&BYCLYVHDd$~}r~U<4 z%Fkc*o9CFqZX~oDf`UGX?!gb4n_kI6A+6bt*NQm0@_tJxbEcG z6(T{`b`*tj60|bjb&TGt5Pg1|;n+D_{rk%k6lwsXGX>#k7@kN-gwFH8bkPfh1hX`- z?dp2Yuj_X18+RbRH8^#tpnh!OF7Ur>z|xP5=zt_s_8^TAH8He2Q;O!3eM=beJ1j(w z0JV7?t~oMNZxzt`i*;xv)T(wneEDRzIq!Lj6cNk)vD$+*N=_1O3-){WdTaz18xIIk ziZ4S46d698{eHIWY6=G{odCyAl&JH@y6e3F>MK_koN6EhfNgblb)~L40fvwVsO@Gu zMGNtvjuet*_e)zO*{Y2LT5TO2Cnx{ZP-l=0@eRF*>f}AgIsCE#%y(>Kj;fI9HjZAK6J6aI?hT z@ehaF@Cv&_@I>%anKp+O8?OFzNy>g4|Qx1byh+4&s1 z7QDG}Gv(E}Y@KC~omd^p(qqk6uR;%hlE^3OG0?E9B4g6;^4ML&AffaXZ226v*|gLY z_@b)WXtU7JP$tHNdfYwZ?_|)4q7JMAzJlbyWo)^oseoY@eM4wx+AG*Pe@P}!&Tdtn zz-v>?dg>JD3yy-q_KTq)e&#R;xxCq1|AW9C&?j>c5*V1A=!Z52g^nXC`G=19h$zjy zm2v9XzWuqYk5DL;%c?gxOeV;-bscJ+5cAm$6?)0ebbpt--d3xqn;O zZ5+erTYwz5j@$jhlpypf0=E?Xa2|B3XxI|8#6HeX>%$p!)01!TPv zhZQYoEo|)UG6A$aJs_FnyROx$U%Ys+QU2L&X%JLH8V$YzNuRG<-IppIa_WGInfVdR zBa4221!4jerFx!e*!Ispy~Uk^wJ?`-GzV^LN$rX9$>?(`zs5!k0W&q0No?EnG~4QX zvsrF*M2EVnYS^zIbFWV;+Ki}I*RAHbn>hZM-J(;G$LI)GOf~tybEX2vI}bElYfDS_ zXNp@TF59L7GABf&e z6RIyx>=JH|PA_4qmGe#=2nuFq?Qk<>CJ4{x#T4YVt+PhTs5p6@skHvFvbi&{A~!v^ z^yo1K%IU^iIzE2%{1+Isn5F+43?C;QoH=s_;af?_$p-*8VN#_je0+Qmz=bGSHMO5M1FLu?H*VhK0ha6a>#!9t zOigI!n_tR0sU0kQ*8B~d_HSseoBmGEoap9G*_&CjQI%qL@zec}#x+ zpRm0&FmN+SFgZ5%DqMWzgMj741|s~!>q`jl14{R0(5--x)CV>L$<@m=G;h$K8<+tQ zd_iPaWS9Z@q6@Mr43xc2k&}`69u&%QtE(YhM!K`#Z|&F!%@&;6skf5tF}g&fdxrfJ z;IT}gevn}YI2~Zal<5I(LwczKHw|QkKi5TT(zkSx{grle(`#$oj0tZ~p5Zn(LqkGR z8X82mSFq{;#(xcr73hoDgJ6UpX4t8v{9uBJED)QhxO7;hU6u=*FJHQpoTZ$x2>8~J z=MgINg_N*$TFq^$+Vu{jpQh`uGEIB=Rn=BEf0~Lm;wJsL5lscLq(C)Z8Wgr1c{tQ& zyF@ebl7^i~xp2rFzy6aPxn0C_0VEQ_IJ`ecQ39d{y|9}GQcRID4%D4=Ft8xgPVSbL z`N6@#eZZqY=$pXMA?3~?8YQ?hXt*CQ0wim2Dx=5V_fNJc)EU>~W3_1E^x{*8wWA)Z z-(HBgAIK|+(N%=C8~q&zU=E%kwcvUY$gIKy7LqU?v;O%obv;D;&6D(_S^z(TB9v|( zxam?0tQ!2Y7uab`v9SX1Nh-miQBhfduq1)}4F|P1IXU?0kB;;q$f!0t*|)` z<45!f5Y%QZPkE6ndmqb{9K~p3{Es}hPp%Zo$Of};4t~}gZ=$1u$;(aXFK9@u8V(|hoo{aZ)eJLY^lU}r@zH^5 zy@z`<4u-{FM#Wh`TsKh?q3#etp%D_8CfQzlu5fZ-8XWo=sK}-bp-8#$Cy>smCdRT#8lbTX7;_NM3kdY8fRdG=Hgu;Eq&duY zT}LcLi{SI{INDZ+?kejzY9Enbvnd04jf#Bsf|>DXshMZk2QD5Aj2?YzEd5&tn`Tr)H4^ket0p!scq;j}vrO zLYxNxjwz|AjL@IGw1L>4sy$e(-rU*g2YeS67M3O-$8Q2+C)9}u}_ZUZlNOa_a~c9)Mpb02)j1 z1LKaXhJH}P?`S~MB6gRQHq`mXt9*#h8--$#v?l!fj~7a(|LEse9=2pKW|X$s;K&&azWlWNuc*tEa7yf827T=vTR|7j|*o1{?MFeJI5M zXiSz7TIQb)2D9HLnA}ow?tE>Zsn zzoX!D0TCUQX<@ISJ~BtoieHLse{Zl&QXiv{_hi%peZ{M-??Qw@_CF)=jRtYp+u6fb z=CnI@v(Jz!2LwC9)>^zgxy8jL54V}%zN1-r6)a7c`xZZA&ste)X3}%nTTU=@bHlr{ zgvQ0?QR_K9hGAF{5o*Ew1tLb@d=aXAu*SKV!Sm3j^i8)IknL(n*u3ew2B^PtzQ?Pr z36#kP=y^r(8YzO9-TQ>meY(u<1o{K*eItKt2k}F@%3XpVbWZ#?#pymjvP6Y8KsDxZ zT-6cU?o&1$sVEdaSiV2%G!>v#XyC)|xN44e7Ong%5&=*#nP~6^=oSX~lXh~n=84$7 zc+CeBVTPpDy)m%?&P6tm%E9%Z4&d5$snT=r4>5E=*Z1H)lAI^Zi?5KozMXA)qRxRf z*k&x6(`?&z^WuNHxv;Z}|3Utve5OUu1=W3o&Bi3Pr+_pH|0W=d;|8? zGb*Pee?l4B1`5IqtagZv5-1Soj02EHR#FN^pfgCfrqH~hqB+E-K+9MJP0ZU_01UNU3x}iU zJI>SdtK2vG{Uy~67d*JKBYS+}?fX$!(dGE%V^hIBEglB9m3`chd8<$io3^}OWjr;j zKXCJP-@!`0U#&kBl<-CKSn?7)c@L;wh|B8BmoIKuY^m8T>ESwef$wDOS7A!%%}~gJ z4v+L%19(8hu!r<+kT4M84`?bi;OlTJyP&u&Aj(!1C4gvfjt`jkd)&c=W0{k;b?o85r034smTU%(b&L;gH zZx$yPN(I#R1|;R^M|MV0K_7kJ2~TJtwsId8-sMBDA!Pr0T0(BA!rGp~a+?d-Ouny{ zJ$0TB93=?41B=uBs>==kAZE5@hiZtN6d1^!23yu2Iq2FnQUDIDCx{gn&}pcl z>&fvE;;1wMP=>r~Qzo8R_}aiqD?;Ben>brZb*3i#3Wdq9){X*@+ULf+{P|O>*kgsjDlZl10Xy3_^g* z5C!YQ`6~wKM_yQ8&)Fvowp1kXAOe4@O&X(IWK#lx6-p7-A3&8*wi- z#xG6s9 z2AUzR6rfyKvPr=kc~O2yB1c(MLP3@E4G6%1kQW*sA8++`G61H+Ved9wHMMX=%L81DpaIjd zn#yiwf)5Cuf~_m@JUKpP;kvtOFK9XVEeT=+;oKu5BV~m?s+8pavK|b7p@>rCgxrr-3D5wX(7T#4J1RagYao`pLmMU;vT551a!!E26x- zyeW`g`-RC%NXOLFRD({tm*b?iwzdzzqIT*4O0y!&K#Bl(4Y8Are)dpN4C)hF!#WFI zO)blDPn<`L05;8JUN=gu#`OS#_Cefcf`AN-=P48rMexT9AMY8av=O$B7IzBgtkoUJ z1J{apq=2RmD~MBlsCW^kbv2Qrtq>%vpx>x~zF^qw0p!v! zEF#7}u>15w4g0J7GYac~2VN&>ZXV41KYsq?dG+fbq5bI)z~4wpd-F8}BUPh4PCy-u z5=aq2uTMNiYABFiw|89Oh(Q3;Ld&fLl>)umX{gcys}(7sC&r4;|A+m@%K&kdxqyts z1ML`W37Ih4gJajG4(s6Eh5;`uvo!b!dXgpmNJ=P15^o?K&f|FBSP1v~zROIQx(bN< z$TQwjh)36$grx$T#16VHQo_NKz$^`;OFtRkNFr$FmA~x3=w-Sw*@*bAw|iu{5#Kd9 zCWF8j`PFKMK^jTaF^>Za#DJ|Jw2kmG_snev z_^0?2#B{!2q4@#U} zF{N@mjl2OWiirgTl&4Ur(5ncZU3Zes(!`tp@!-T-i-E_K{W^0_m-~s(Yd@wIgRh`9 zD`RJ$R~0J|ZZE3H>giEAh5==xcmsjv4TcuOUlX(R*ZS2(j?$K|E-}~kTShpXgk_I< zrkT!85}GPRiI{Xkf zPL9RFbm%X}4EhSDtEFB~^`-CQrbjz_wvV1T1gcU9W0U&d z)nh)4x6NIJXJ`I9KM4K+KPcwUm?SOFKHtpraO;tq>xHze-Y<9-6r(PB;Fvm>u%cLhD|}u@Dqp)3ZuS~9RM=|mx{dewp7T%A;#%(J zen_j2?;;~VNxeVkp93MF^6c-$N_Y<$3Sts;vVY&6`pBL)&QPHJt>gAf+v4FIM=J&S>+9o@0Z8Y@e|x!i3-OHPZ+lQ3S8svVL+c$yaeuRI67-Qn@p%xgU{@K zeR(lSeSnR*);tLg_=-yZT}XY#M9DkX69C$@XC z0Jo$FKH>arEF@N)g+ed^28ELGdt}+#g19!2143}9VtfaReM>?0(b4Uxq##cA{t$iYtk-`{Ry-@&VKibeDwlW zAit_k!gvr5qsvK}JncvfIXOW?E)DyP0Z?f6?|I?a3N4(oLWT!Wf%TB_1ON--KL=4p zCRop7|8=ujLDeF=xG7M-=goPm(>;o@z*tMGm8m9hj#$B%1#LLq!|q+Yi=AO%QhTFP zj~%)h$Fa5JPd}qUk4n4^8u}aX^n%=f%q+dRx!D5~KMzdXs`)yP#yx=6!NcJX#B>uI zTi?9i+|*Q6tON1!dbF%}*z@5iyxCAQ>CP41Q;FTm<%@Be0Y+o}3?1Lc+T z&qzfV&;<5~_p0Z9r(g;a?BzLgn{%$a{O; z16rvFZ<052-k0<6E1_+HyW5=~w|4ASC@DMMV^J@tPV;fSD93KYWd-8LZJOzsU39@G zFjZK*Zvh?{WI#bIO>n{mQlq6@D=cTooR^t7?1)0VzS#Kk{Htn=sj8`%SX5Au>|9uE z?iaCP@XWr(O>%LmgZGTHgwtF2#V7yNX)$bYNN^Y>$oE={QCE-bm!BcReAP52=7#u@ zY)Kv~lzZu5MQ8EsRZV_yj0xFuQ@Qsib zBJ3?7GU|w0WZ888^iTOg82W9|l8zMaH zRPIDxAx?W&P`wkGwmzPzmp!Sx&b6b^Q}#yANGB?&bNixK@`L=Vsc{3N^Cfw!!E5My z)f1(U-P~%DQ&M#GNbN={8veJq{{DZ6>ntpF58*`XMA7Vw_Mxzm(Z)LdPi2iCW4z}= z^LdA(y9B5-DG&}3kR%xR=@V zQ7z$<9rMQXWT#OTl#Jw+qOg5a8*nIr^tT*N0csisT&wh*abzL)#Nb;%0<^hZtq(Ix zjnW5Tllk1R(eudpzu=2xs-19K{RA2Di;8lgge)zj;$=DSg#?`!)!<2z#nW)MJsm@b(sm|C4mhzKpAY({Y-6l@%(M^DSc7dBUcLA*y@tS1-b;=Gi%e zivDAtJnTAOwQi%rC7n~!CPQ40IvQyQNOvoaBqz`Pw+w3-_I0s&IqoOGVL9)`^fBMyRvQ$ z$AcEC95A3q3miO9l*p`a3nWId3cw{7u;m4Mo=5i0b}*X3ArCVUS&_mU$S9IOKmW!Z z*UpC=#eg#nOj$tcXDg-oK7UP!dO>RuEqO&(EUR>9V))N~Je{qNY~a+CVRwjQsdY;s z3Y9NzJtcPYNnju1(|FN<0;C`ZJRlq!s6qxML}En@)?f+6k)=#p0_EAUM5B!X9X?I+ z=N+Fj2UkFn`UcGN5~f+3y58`vFhj3opwo7Y^0(@Kubh6oBr zt{@4IYTB9fAVs#4=_j7=Zq;eg6OTJ;#`ib3#ubh$rU$fc_e_7KM~}A$v?bj`iE7$3 z2M|jLxAHWSvdIuq2{M1wMw}1hJLwe2XTEpWBqIxw#%*<(mve3~jR+k1e zyb0p%#MgD*Y1swef7W@)Ku&I&KGn9`_pt!2loRRTOG(%D_*{xIahZI z)X(2``TkrKh-cizrHTm|{>f+`u%gFdeGwz3JT^JyM9-lFhymo-gGn3;MRWbZpaLi| zspNCZcdJ4#ZIlSSSG;?g2yeRQmF}{>!veFwV}QYI%tG1#<$XTxEwr+HmHfbuLfKuf zuqpPNEB_o201fQ!H(C_xxAE_r>Lwk*;xh7Wyj7t$Mm@FtwfKyA*diku4W1vUsrJ=T zazvi;x@DLd8l&5K$O}Vz$fzavj$gF}3KbDz`rI2o?nm#>mGZPSTdn`!xnVk82>VRb zS<8{-1Ozj9y< zX$$+dO|Vk&=q%lIL%_?Smyt`Nl(VTQGa1L%(k5D&^@sNomL1sc zGINYajOyW|T^zI1c8^C3w}{~fDE z0wAbz?e9!)u&hb(%@v$dk7Eh4{?)*$154=%O(cj9e)%R)=ADKo+#;8A#_Osi^J1QawSn;j zCFASrL4fjV{+vafJ;OMjIljxXaAdXf28^d*FIG=C`w=b2gTYS8Fl728 z`#b$lgXlA$K_G2e<;^40dhGp&p zH_-H2bI%xzjw8s&`82%Fka@+ty`ZG+Ue(82br~*u@1?B)JQ95UKc1|;>4pNhP#c# zgirlc;*BC>BdW3awXt#W(|p#otzmIi3L-aME#qCy2+mJXQ>nbsHgd~8fK6!-pLspQ zDzFmU%UjLQXH~9|+8|mUE@n+QKzh!hPKZx^N5r9`oT>qJ{KtowuQ}Yp z#*i6|cRP$;3ReBkCPJr^YpML82sF1IX(U1>(NJcuIq z>2g$5QGwoCNqbLe%59(X7UDy0yWjcKnE4YlHr{^u)k@-*e_P*E-`QDnL`AbPBqlDS zLN_C}3}dDzS9dIInSb^U!_g@t!0%xpaT&#IS7d}|4trNf4^O#0m{?fd+;M2miB?G0e2U`4&UpjFLl^NgsI%TcjZ5kY*!_7&lph>-mJlf#3;Qw6! zpCqD_Bx&zwV7Bp?5%tc=was;;e#Tysv^*ht+>Mn_Pkc34y?_C1DKzOel-!N|lUz_* zTeEE_{ql2L##K;fHg>{yaSB&14L^m2egZ7Mv(P>N^)r-Oev9vcWC@j=VyDv8ZNA~> zQ=fr4Z+*RFp{wKDB^q`gTjTPfR|gXmLmpbDXg05x4)G->2X{S2!OP8Nkgd@d-gU<7 zxtrVc(TNldEpm11n+Gk*MS0k3*ebG~@8VJsgcZ6*fx1tWIy@Vxg-&juP?TyCi7K~# zB=i5FA|n205M_Ozkx?_>l)0dT{*}pqT6n9slse><;h;i%uthHw8?)+8@tm)|J4waw zSF!Egh3};KBjVFymj!2xv>Uk%QKFL5OLLqeo{U6i_Op!MohABM7)^oV+bsH}R9SRc zMKe5_=e5Z|)8&`#jsN@Oy;?p)e!|!Phd%pj;!>nH~0jbf? zSw3Q5o%VONoGz?Zc*{CDJ0p4}9>jehzWf$sH8;TVM8SO^7yn*Ns9&h1%xq7vej#RJXCXf1C%UcOFO6CM0P-dq zHrnS>kIWK^*Jjv8jaE}iB63A5E0Sh@$2cQZsqG)YDG55AK5(9pM}dh-NOSVZ^Y-`| z;a4Es!`j;!2y_DRW^sQ-H~|?5>n}wj`N5IoG#L!Kt=o}l2heaCD%DD`m7hWHMBJ=U z)F#HNRxM&yqR)r+?I3w1V5herlPglL-enxbh+^bZNUxJ9Hy$Vl5#W}V+N~dAjjI$B zw3#UO04em$YL!G4QslpSiD=YQ1k?;H+!qSWpqtb~;Iv*}bf?_x$QYW{i?_3rJ*86A zfXFi;YWc0ulQ&cqHSK_403}l!E!t4;Wn*jW`TlKK+~9e4i@sUQD|5j9qL0E?z=CSM zW#-e;WkI{I)PU@5{R14RU$q*|RAccok)wzr$B*tTObv-i~BC1Qd`$| z!SJ%;t1?+dKl5yo*GyulP(M^26S;Pw%`(&cKB898{do2mfU*bEuP|hJnvN^{(oR5x zOGmxsDm(V<++2^*)=~B1BWUAyd<)k0co%{Y33Lx zloUl-L?lcK>Q?*A&$@o^HE1z(MhVID)0G5sM;1~4pHEI`F2r`_{e;C{Zox=woxOFh z#HxF7x2L6W1jtUsWAi|fyLE4Zjs$*;*3gai`W?#Sf(3!aE@~&To#nwdr8Hj>lDYxp zC#{^E=DVyU?$*KMH@iei1v!gRU(ocjt=Z5}v}|V*mg9VdKw8F6ef|96uzKPfA?8I% zUASzR*8y4Q#HkcG%-DN)47DO$A~ih*{D2r>chYvq>7xvxaMqQ2m_o%+{#kNGxguH250>lb@A5`d&Y-4EC5Scwy!mL}%g(Oj z2B&lIFU!-kvZ!ufEkA1gWpBsJZiAf(2zXqQ>$(l{9I zfQ=zIw+y#|atoJOJK#tV{zP1RGDF?TR{C-(#7ol#378W4YJ&4mu(r|-$i*UAX5s#A z9+8*8H-VjXW^vM%Ahi%}5r7kDdUipZ-DX~E#; z4vNEf95JSwbNxKJ!vWm^EU}wRKbD?iT~=SY=$yER;~VhYFYkYz`VJr(0Apm+q2o NWBUd9%6%f!{sm`4u~`5B literal 0 HcmV?d00001 diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index e99cb3f628d..9e3dd5cc48e 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -47,7 +47,7 @@ ) from qiskit.circuit import Parameter, Qubit, Clbit, IfElseOp, SwitchCaseOp from qiskit.circuit.library import IQP -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import random_clifford from qiskit.quantum_info.random import random_unitary from qiskit.utils import optionals @@ -2300,6 +2300,59 @@ def test_no_qreg_names_after_layout(self): ) self.assertGreaterEqual(ratio, self.threshold) + def test_if_else_standalone_var(self): + """Test if/else with standalone Var.""" + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", False) + qc.store(a, 128) + with qc.if_test(expr.logic_not(b)): + # Mix old-style and new-style. + with qc.if_test(expr.equal(b, qc.clbits[0])): + qc.cx(0, 1) + c = qc.add_var("c", b) + with qc.if_test(expr.logic_and(c, expr.equal(a, 128))): + qc.h(0) + fname = "if_else_standalone_var.png" + self.circuit_drawer(qc, output="mpl", filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, self.threshold) + + def test_switch_standalone_var(self): + """Test switch with standalone Var.""" + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", expr.lift(5, a.type)) + with qc.switch(expr.bit_not(a)) as case: + with case(0): + with qc.switch(b) as case2: + with case2(2): + qc.cx(0, 1) + with case2(case2.DEFAULT): + qc.cx(1, 0) + with case(case.DEFAULT): + c = qc.add_var("c", expr.equal(a, b)) + with qc.if_test(c): + qc.h(0) + fname = "switch_standalone_var.png" + self.circuit_drawer(qc, output="mpl", filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, self.threshold) + if __name__ == "__main__": unittest.main(verbosity=1) From 9367f7b6f4bfcd523ad8919cd65e2f6c4cad2be2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 1 May 2024 19:20:09 -0400 Subject: [PATCH 021/159] Restrict iteration of commutation passes to just qubits (#12318) The CommutativeCancellation and CommutationAnalysis passes were previously iterating over the full set of wires in the DAGCircuit. However the wire list contains classical bits and also classical variables after #12204 merges. The only thing these passes are concerned about are computing whether quantum operations commute and therefore don't need to work with any classical components in the DAG. In general these exta iterations were just no-ops because there wouldn't be anything evaluated on the wires, but doing this will avoid any potential overhead and limiting the search space to just where there is potential work to do. --- qiskit/transpiler/passes/optimization/commutation_analysis.py | 4 ++-- .../passes/optimization/commutative_cancellation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 751e3d8d4f5..eddb659f0a2 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -47,7 +47,7 @@ def run(self, dag): # self.property_set['commutation_set'][wire][(node, wire)] will give the # commutation set that contains node. - for wire in dag.wires: + for wire in dag.qubits: self.property_set["commutation_set"][wire] = [] # Add edges to the dictionary for each qubit @@ -56,7 +56,7 @@ def run(self, dag): self.property_set["commutation_set"][(node, edge_wire)] = -1 # Construct the commutation set - for wire in dag.wires: + for wire in dag.qubits: for current_gate in dag.nodes_on_wire(wire): diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index b0eb6bd2413..396186fa95c 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -99,7 +99,7 @@ def run(self, dag): # - For 2qbit gates the key: (gate_type, first_qbit, sec_qbit, first commutation_set_id, # sec_commutation_set_id), the value is the list gates that share the same gate type, # qubits and commutation sets. - for wire in dag.wires: + for wire in dag.qubits: wire_commutation_set = self.property_set["commutation_set"][wire] for com_set_idx, com_set in enumerate(wire_commutation_set): From 676a5ed98d84151b7bb5948d431c27202d9eae5f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 2 May 2024 02:25:25 +0100 Subject: [PATCH 022/159] Support standalone `Var`s in OQ3 exporter (#12308) * Support standalone `Var`s in OQ3 exporter This adds the remaining support needed for the OpenQASM 3 exporter to support standalone variables. The way the circuit model handles closures over these variables makes it much easier to support these than it was to handle free-form control-flow operations. This PR somewhat refactors small parts of the exporter to better isolate the "top-level program" statement construction and analysis from the "build a scoped set of instructions" logic, which makes it rather easier to handle things like declaring IO variables only in the global scope, but locally declared variables in _all_ relevant scopes. * Remove references to QSS * Clarify comment about forward declarations * Add test for parameter/gate clash resolution --- qiskit/qasm3/exporter.py | 261 +++++++++++------- ...parameter-gate-clash-34ef7b0383849a78.yaml | 7 + test/python/qasm3/test_export.py | 172 +++++++++++- 3 files changed, 336 insertions(+), 104 deletions(-) create mode 100644 releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 329d09830ac..b85e35e22a5 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -33,6 +33,7 @@ Qubit, Reset, Delay, + Store, ) from qiskit.circuit.bit import Bit from qiskit.circuit.classical import expr, types @@ -62,7 +63,6 @@ _RESERVED_KEYWORDS = frozenset( { "OPENQASM", - "U", "angle", "array", "barrier", @@ -239,6 +239,7 @@ class GlobalNamespace: def __init__(self, includelist, basis_gates=()): self._data = {gate: self.BASIS_GATE for gate in basis_gates} + self._data["U"] = self.BASIS_GATE for includefile in includelist: if includefile == "stdgates.inc": @@ -282,6 +283,10 @@ def __contains__(self, instruction): return True return False + def has_symbol(self, name: str) -> bool: + """Whether a symbol's name is present in the table.""" + return name in self._data + def register(self, instruction): """Register an instruction in the namespace""" # The second part of the condition is a nasty hack to ensure that gates that come with at @@ -324,7 +329,7 @@ def register(self, instruction): class QASM3Builder: """QASM3 builder constructs an AST from a QuantumCircuit.""" - builtins = (Barrier, Measure, Reset, Delay, BreakLoopOp, ContinueLoopOp) + builtins = (Barrier, Measure, Reset, Delay, BreakLoopOp, ContinueLoopOp, Store) loose_bit_prefix = "_bit" loose_qubit_prefix = "_qubit" gate_parameter_prefix = "_gate_p" @@ -348,14 +353,12 @@ def __init__( self.includeslist = includeslist # `_global_io_declarations` and `_global_classical_declarations` are stateful, and any # operation that needs a parameter can append to them during the build. We make all - # classical declarations global because the IBM QSS stack (our initial consumer of OQ3 - # strings) prefers declarations to all be global, and it's valid OQ3, so it's not vendor + # classical declarations global because the IBM qe-compiler stack (our initial consumer of + # OQ3 strings) prefers declarations to all be global, and it's valid OQ3, so it's not vendor # lock-in. It's possibly slightly memory inefficient, but that's not likely to be a problem # in the near term. self._global_io_declarations = [] - self._global_classical_declarations = [] - self._gate_to_declare = {} - self._opaque_to_declare = {} + self._global_classical_forward_declarations = [] # An arbitrary counter to help with generation of unique ids for symbol names when there are # clashes (though we generally prefer to keep user names if possible). self._counter = itertools.count() @@ -367,18 +370,15 @@ def __init__( def _unique_name(self, prefix: str, scope: _Scope) -> str: table = scope.symbol_map name = basename = _escape_invalid_identifier(prefix) - while name in table or name in _RESERVED_KEYWORDS: + while name in table or name in _RESERVED_KEYWORDS or self.global_namespace.has_symbol(name): name = f"{basename}__generated{next(self._counter)}" return name def _register_gate(self, gate): self.global_namespace.register(gate) - self._gate_to_declare[id(gate)] = gate def _register_opaque(self, instruction): - if instruction not in self.global_namespace: - self.global_namespace.register(instruction) - self._opaque_to_declare[id(instruction)] = instruction + self.global_namespace.register(instruction) def _register_variable(self, variable, scope: _Scope, name=None) -> ast.Identifier: """Register a variable in the symbol table for the given scope, returning the name that @@ -399,6 +399,10 @@ def _register_variable(self, variable, scope: _Scope, name=None) -> ast.Identifi raise QASM3ExporterError( f"tried to reserve '{name}', but it is already used by '{table[name]}'" ) + if self.global_namespace.has_symbol(name): + raise QASM3ExporterError( + f"tried to reserve '{name}', but it is already used by a gate" + ) else: name = self._unique_name(variable.name, scope) identifier = ast.Identifier(name) @@ -441,15 +445,66 @@ def build_header(self): def build_program(self): """Builds a Program""" - self.hoist_declarations(self.global_scope(assert_=True).circuit.data) - return ast.Program(self.build_header(), self.build_global_statements()) + circuit = self.global_scope(assert_=True).circuit + if circuit.num_captured_vars: + raise QASM3ExporterError( + "cannot export an inner scope with captured variables as a top-level program" + ) + header = self.build_header() - def hoist_declarations(self, instructions): - """Walks the definitions in gates/instructions to make a list of gates to declare.""" + opaques_to_declare, gates_to_declare = self.hoist_declarations( + circuit.data, opaques=[], gates=[] + ) + opaque_definitions = [ + self.build_opaque_definition(instruction) for instruction in opaques_to_declare + ] + gate_definitions = [ + self.build_gate_definition(instruction) for instruction in gates_to_declare + ] + + # Early IBM runtime paramterisation uses unbound `Parameter` instances as `input` variables, + # not the explicit realtime `Var` variables, so we need this explicit scan. + self.hoist_global_parameter_declarations() + # Qiskit's clbits and classical registers need to get mapped to implicit OQ3 variables, but + # only if they're in the top-level circuit. The QuantumCircuit data model is that inner + # clbits are bound to outer bits, and inner registers must be closing over outer ones. + self.hoist_classical_register_declarations() + # We hoist registers before new-style vars because registers are an older part of the data + # model (and used implicitly in PrimitivesV2 outputs) so they get the first go at reserving + # names in the symbol table. + self.hoist_classical_io_var_declarations() + + # Similarly, QuantumCircuit qubits/registers are only new variables in the global scope. + quantum_declarations = self.build_quantum_declarations() + # This call has side-effects - it can populate `self._global_io_declarations` and + # `self._global_classical_declarations` as a courtesy to the qe-compiler that prefers our + # hacky temporary `switch` target variables to be globally defined. + main_statements = self.build_current_scope() + + statements = [ + statement + for source in ( + # In older versions of the reference OQ3 grammar, IO declarations had to come before + # anything else, so we keep doing that as a courtesy. + self._global_io_declarations, + opaque_definitions, + gate_definitions, + self._global_classical_forward_declarations, + quantum_declarations, + main_statements, + ) + for statement in source + ] + return ast.Program(header, statements) + + def hoist_declarations(self, instructions, *, opaques, gates): + """Walks the definitions in gates/instructions to make a list of gates to declare. + + Mutates ``opaques`` and ``gates`` in-place if given, and returns them.""" for instruction in instructions: if isinstance(instruction.operation, ControlFlowOp): for block in instruction.operation.blocks: - self.hoist_declarations(block.data) + self.hoist_declarations(block.data, opaques=opaques, gates=gates) continue if instruction.operation in self.global_namespace or isinstance( instruction.operation, self.builtins @@ -461,15 +516,20 @@ def hoist_declarations(self, instructions): # tree, but isn't an OQ3 built-in. We use `isinstance` because we haven't fully # fixed what the name/class distinction is (there's a test from the original OQ3 # exporter that tries a naming collision with 'cx'). - if instruction.operation not in self.global_namespace: - self._register_gate(instruction.operation) - if instruction.operation.definition is None: + self._register_gate(instruction.operation) + gates.append(instruction.operation) + elif instruction.operation.definition is None: self._register_opaque(instruction.operation) + opaques.append(instruction.operation) elif not isinstance(instruction.operation, Gate): raise QASM3ExporterError("Exporting non-unitary instructions is not yet supported.") else: - self.hoist_declarations(instruction.operation.definition.data) + self.hoist_declarations( + instruction.operation.definition.data, opaques=opaques, gates=gates + ) self._register_gate(instruction.operation) + gates.append(instruction.operation) + return opaques, gates def global_scope(self, assert_=False): """Return the global circuit scope that is used as the basis of the full program. If @@ -540,40 +600,6 @@ def build_includes(self): """Builds a list of included files.""" return [ast.Include(filename) for filename in self.includeslist] - def build_global_statements(self) -> List[ast.Statement]: - """Get a list of the statements that form the global scope of the program.""" - definitions = self.build_definitions() - # These two "declarations" functions populate stateful variables, since the calls to - # `build_quantum_instructions` might also append to those declarations. - self.build_parameter_declarations() - self.build_classical_declarations() - context = self.global_scope(assert_=True).circuit - quantum_declarations = self.build_quantum_declarations() - quantum_instructions = self.build_quantum_instructions(context.data) - - return [ - statement - for source in ( - # In older versions of the reference OQ3 grammar, IO declarations had to come before - # anything else, so we keep doing that as a courtesy. - self._global_io_declarations, - definitions, - self._global_classical_declarations, - quantum_declarations, - quantum_instructions, - ) - for statement in source - ] - - def build_definitions(self): - """Builds all the definition.""" - ret = [] - for instruction in self._opaque_to_declare.values(): - ret.append(self.build_opaque_definition(instruction)) - for instruction in self._gate_to_declare.values(): - ret.append(self.build_gate_definition(instruction)) - return ret - def build_opaque_definition(self, instruction): """Builds an Opaque gate definition as a CalibrationDefinition""" # We can't do anything sensible with this yet, so it's better to loudly say that. @@ -604,7 +630,7 @@ def build_gate_definition(self, gate): self.push_context(gate.definition) signature = self.build_gate_signature(gate) - body = ast.QuantumBlock(self.build_quantum_instructions(gate.definition.data)) + body = ast.QuantumBlock(self.build_current_scope()) self.pop_context() return ast.QuantumGateDefinition(signature, body) @@ -627,8 +653,10 @@ def build_gate_signature(self, gate): ] return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None) - def build_parameter_declarations(self): - """Builds lists of the input, output and standard variables used in this program.""" + def hoist_global_parameter_declarations(self): + """Extend ``self._global_io_declarations`` and ``self._global_classical_declarations`` with + any implicit declarations used to support the early IBM efforts to use :class:`.Parameter` + as an input variable.""" global_scope = self.global_scope(assert_=True) for parameter in global_scope.circuit.parameters: parameter_name = self._register_variable(parameter, global_scope) @@ -640,11 +668,13 @@ def build_parameter_declarations(self): if isinstance(declaration, ast.IODeclaration): self._global_io_declarations.append(declaration) else: - self._global_classical_declarations.append(declaration) + self._global_classical_forward_declarations.append(declaration) - def build_classical_declarations(self): - """Extend the global classical declarations with AST nodes declaring all the classical bits - and registers. + def hoist_classical_register_declarations(self): + """Extend the global classical declarations with AST nodes declaring all the global-scope + circuit :class:`.Clbit` and :class:`.ClassicalRegister` instances. Qiskit's data model + doesn't involve the declaration of *new* bits or registers in inner scopes; only the + :class:`.expr.Var` mechanism allows that. The behaviour of this function depends on the setting ``allow_aliasing``. If this is ``True``, then the output will be in the same form as the output of @@ -670,12 +700,14 @@ def build_classical_declarations(self): ) for i, clbit in enumerate(scope.circuit.clbits) ) - self._global_classical_declarations.extend(clbits) - self._global_classical_declarations.extend(self.build_aliases(scope.circuit.cregs)) + self._global_classical_forward_declarations.extend(clbits) + self._global_classical_forward_declarations.extend( + self.build_aliases(scope.circuit.cregs) + ) return # If we're here, we're in the clbit happy path where there are no clbits that are in more # than one register. We can output things very naturally. - self._global_classical_declarations.extend( + self._global_classical_forward_declarations.extend( ast.ClassicalDeclaration( ast.BitType(), self._register_variable( @@ -691,10 +723,26 @@ def build_classical_declarations(self): scope.symbol_map[bit] = ast.SubscriptedIdentifier( name.string, ast.IntegerLiteral(i) ) - self._global_classical_declarations.append( + self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.BitArrayType(len(register)), name) ) + def hoist_classical_io_var_declarations(self): + """Hoist the declarations of classical IO :class:`.expr.Var` nodes into the global state. + + Local :class:`.expr.Var` declarations are handled by the regular local-block scope builder, + and the :class:`.QuantumCircuit` data model ensures that the only time an IO variable can + occur is in an outermost block.""" + scope = self.global_scope(assert_=True) + for var in scope.circuit.iter_input_vars(): + self._global_io_declarations.append( + ast.IODeclaration( + ast.IOModifier.INPUT, + _build_ast_type(var.type), + self._register_variable(var, scope), + ) + ) + def build_quantum_declarations(self): """Return a list of AST nodes declaring all the qubits in the current scope, and all the alias declarations for these qubits.""" @@ -760,21 +808,37 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen out.append(ast.AliasStatement(name, ast.IndexSet(elements))) return out - def build_quantum_instructions(self, instructions): - """Builds a list of call statements""" - ret = [] - for instruction in instructions: - if isinstance(instruction.operation, ForLoopOp): - ret.append(self.build_for_loop(instruction)) - continue - if isinstance(instruction.operation, WhileLoopOp): - ret.append(self.build_while_loop(instruction)) - continue - if isinstance(instruction.operation, IfElseOp): - ret.append(self.build_if_statement(instruction)) - continue - if isinstance(instruction.operation, SwitchCaseOp): - ret.extend(self.build_switch_statement(instruction)) + def build_current_scope(self) -> List[ast.Statement]: + """Build the instructions that occur in the current scope. + + In addition to everything literally in the circuit's ``data`` field, this also includes + declarations for any local :class:`.expr.Var` nodes. + """ + scope = self.current_scope() + + # We forward-declare all local variables uninitialised at the top of their scope. It would + # be nice to declare the variable at the point of first store (so we can write things like + # `uint[8] a = 12;`), but there's lots of edge-case logic to catch with that around + # use-before-definition errors in the OQ3 output, for example if the user has side-stepped + # the `QuantumCircuit` API protection to produce a circuit that uses an uninitialised + # variable, or the initial write to a variable is within a control-flow scope. (It would be + # easier to see the def/use chain needed to do this cleanly if we were using `DAGCircuit`.) + statements = [ + ast.ClassicalDeclaration(_build_ast_type(var.type), self._register_variable(var, scope)) + for var in scope.circuit.iter_declared_vars() + ] + for instruction in scope.circuit.data: + if isinstance(instruction.operation, ControlFlowOp): + if isinstance(instruction.operation, ForLoopOp): + statements.append(self.build_for_loop(instruction)) + elif isinstance(instruction.operation, WhileLoopOp): + statements.append(self.build_while_loop(instruction)) + elif isinstance(instruction.operation, IfElseOp): + statements.append(self.build_if_statement(instruction)) + elif isinstance(instruction.operation, SwitchCaseOp): + statements.extend(self.build_switch_statement(instruction)) + else: # pragma: no cover + raise RuntimeError(f"unhandled control-flow construct: {instruction.operation}") continue # Build the node, ignoring any condition. if isinstance(instruction.operation, Gate): @@ -795,6 +859,13 @@ def build_quantum_instructions(self, instructions): ] elif isinstance(instruction.operation, Delay): nodes = [self.build_delay(instruction)] + elif isinstance(instruction.operation, Store): + nodes = [ + ast.AssignmentStatement( + self.build_expression(instruction.operation.lvalue), + self.build_expression(instruction.operation.rvalue), + ) + ] elif isinstance(instruction.operation, BreakLoopOp): nodes = [ast.BreakStatement()] elif isinstance(instruction.operation, ContinueLoopOp): @@ -803,16 +874,16 @@ def build_quantum_instructions(self, instructions): nodes = [self.build_subroutine_call(instruction)] if instruction.operation.condition is None: - ret.extend(nodes) + statements.extend(nodes) else: body = ast.ProgramBlock(nodes) - ret.append( + statements.append( ast.BranchingStatement( self.build_expression(_lift_condition(instruction.operation.condition)), body, ) ) - return ret + return statements def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingStatement: """Build an :obj:`.IfElseOp` into a :obj:`.ast.BranchingStatement`.""" @@ -820,14 +891,14 @@ def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingSt true_circuit = instruction.operation.blocks[0] self.push_scope(true_circuit, instruction.qubits, instruction.clbits) - true_body = self.build_program_block(true_circuit.data) + true_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() if len(instruction.operation.blocks) == 1: return ast.BranchingStatement(condition, true_body, None) false_circuit = instruction.operation.blocks[1] self.push_scope(false_circuit, instruction.qubits, instruction.clbits) - false_body = self.build_program_block(false_circuit.data) + false_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.BranchingStatement(condition, true_body, false_body) @@ -838,7 +909,7 @@ def build_switch_statement(self, instruction: CircuitInstruction) -> Iterable[as target = self._reserve_variable_name( ast.Identifier(self._unique_name("switch_dummy", global_scope)), global_scope ) - self._global_classical_declarations.append( + self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.IntType(), target, None) ) @@ -851,7 +922,7 @@ def case(values, case_block): for v in values ] self.push_scope(case_block, instruction.qubits, instruction.clbits) - case_body = self.build_program_block(case_block.data) + case_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return values, case_body @@ -871,7 +942,7 @@ def case(values, case_block): default = None for values, block in instruction.operation.cases_specifier(): self.push_scope(block, instruction.qubits, instruction.clbits) - case_body = self.build_program_block(block.data) + case_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() if CASE_DEFAULT in values: # Even if it's mixed in with other cases, we can skip them and only output the @@ -891,7 +962,7 @@ def build_while_loop(self, instruction: CircuitInstruction) -> ast.WhileLoopStat condition = self.build_expression(_lift_condition(instruction.operation.condition)) loop_circuit = instruction.operation.blocks[0] self.push_scope(loop_circuit, instruction.qubits, instruction.clbits) - loop_body = self.build_program_block(loop_circuit.data) + loop_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.WhileLoopStatement(condition, loop_body) @@ -921,7 +992,7 @@ def build_for_loop(self, instruction: CircuitInstruction) -> ast.ForLoopStatemen "The values in OpenQASM 3 'for' loops must all be integers, but received" f" '{indexset}'." ) from None - body_ast = self.build_program_block(loop_circuit) + body_ast = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast) @@ -961,10 +1032,6 @@ def build_integer(self, value) -> ast.IntegerLiteral: raise QASM3ExporterError(f"'{value}' is not an integer") # pragma: no cover return ast.IntegerLiteral(int(value)) - def build_program_block(self, instructions): - """Builds a ProgramBlock""" - return ast.ProgramBlock(self.build_quantum_instructions(instructions)) - def _rebind_scoped_parameters(self, expression): """If the input is a :class:`.ParameterExpression`, rebind any internal :class:`.Parameter`\\ s so that their names match their names in the scope. Other inputs @@ -1008,8 +1075,8 @@ def _infer_variable_declaration( This is very simplistic; it assumes all parameters are real numbers that need to be input to the program, unless one is used as a loop variable, in which case it shouldn't be declared at all, - because the ``for`` loop declares it implicitly (per the Qiskit/QSS reading of the OpenQASM - spec at Qiskit/openqasm@8ee55ec). + because the ``for`` loop declares it implicitly (per the Qiskit/qe-compiler reading of the + OpenQASM spec at openqasm/openqasm@8ee55ec). .. note:: diff --git a/releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml b/releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml new file mode 100644 index 00000000000..217fbc46412 --- /dev/null +++ b/releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :class:`.Parameter` instances used as stand-ins for ``input`` variables in + OpenQASM 3 programs will now have their names escaped to avoid collisions + with built-in gates during the export to OpenQASM 3. Previously there + could be a naming clash, and the exporter would generate invalid OpenQASM 3. diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 3bb1667992a..8589576441a 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -24,7 +24,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile from qiskit.circuit import Parameter, Qubit, Clbit, Instruction, Gate, Delay, Barrier -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import CASE_DEFAULT from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError, ExperimentalFeatures from qiskit.qasm3.exporter import QASM3Builder @@ -948,7 +948,7 @@ def test_old_alias_classical_registers_option(self): def test_simple_for_loop(self): """Test that a simple for loop outputs the expected result.""" - parameter = Parameter("x") + parameter = Parameter("my_x") loop_body = QuantumCircuit(1) loop_body.rx(parameter, 0) loop_body.break_loop() @@ -978,8 +978,8 @@ def test_simple_for_loop(self): def test_nested_for_loop(self): """Test that a for loop nested inside another outputs the expected result.""" - inner_parameter = Parameter("x") - outer_parameter = Parameter("y") + inner_parameter = Parameter("my_x") + outer_parameter = Parameter("my_y") inner_body = QuantumCircuit(2) inner_body.rz(inner_parameter, 0) @@ -1024,9 +1024,9 @@ def test_nested_for_loop(self): def test_regular_parameter_in_nested_for_loop(self): """Test that a for loop nested inside another outputs the expected result, including defining parameters that are used in nested loop scopes.""" - inner_parameter = Parameter("x") - outer_parameter = Parameter("y") - regular_parameter = Parameter("t") + inner_parameter = Parameter("my_x") + outer_parameter = Parameter("my_y") + regular_parameter = Parameter("my_t") inner_body = QuantumCircuit(2) inner_body.h(0) @@ -1471,6 +1471,17 @@ def test_parameters_and_registers_cannot_have_naming_clashes(self): self.assertIn("clash", parameter_name["name"]) self.assertNotEqual(register_name["name"], parameter_name["name"]) + def test_parameters_and_gates_cannot_have_naming_clashes(self): + """Test that parameters are renamed to avoid collisions with gate names.""" + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.rz(Parameter("rz"), 0) + + out_qasm = dumps(qc) + parameter_name = self.scalar_parameter_regex.search(out_qasm) + self.assertTrue(parameter_name) + self.assertIn("rz", parameter_name["name"]) + self.assertNotEqual(parameter_name["name"], "rz") + # Not necessarily all the reserved keywords, just a sensibly-sized subset. @data("bit", "const", "def", "defcal", "float", "gate", "include", "int", "let", "measure") def test_reserved_keywords_as_names_are_escaped(self, keyword): @@ -1736,6 +1747,145 @@ def test_no_unnecessary_cast(self): bit[8] cr; if (cr == 1) { } +""" + self.assertEqual(dumps(qc), expected) + + def test_var_use(self): + """Test that input and declared vars work in simple local scopes and can be set.""" + qc = QuantumCircuit() + a = qc.add_input("a", types.Bool()) + b = qc.add_input("b", types.Uint(8)) + qc.store(a, expr.logic_not(a)) + qc.store(b, expr.bit_and(b, 8)) + qc.add_var("c", expr.bit_not(b)) + # All inputs should come first, regardless of declaration order. + qc.add_input("d", types.Bool()) + + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool a; +input uint[8] b; +input bool d; +uint[8] c; +a = !a; +b = b & 8; +c = ~b; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_use_in_scopes(self): + """Test that usage of `Var` nodes works in capturing scopes.""" + qc = QuantumCircuit(2, 2) + a = qc.add_input("a", types.Bool()) + b_outer = qc.add_var("b", expr.lift(5, types.Uint(16))) + with qc.if_test(expr.logic_not(a)) as else_: + qc.store(b_outer, expr.bit_not(b_outer)) + qc.h(0) + with else_: + # Shadow of the same type. + qc.add_var("b", expr.lift(7, b_outer.type)) + with qc.while_loop(a): + # Shadow of a different type. + qc.add_var("b", a) + with qc.switch(b_outer) as case: + with case(0): + qc.store(b_outer, expr.lift(3, b_outer.type)) + with case(case.DEFAULT): + qc.add_var("b", expr.logic_not(a)) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool a; +bit[2] c; +int switch_dummy; +qubit[2] q; +uint[16] b; +b = 5; +if (!a) { + b = ~b; + h q[0]; +} else { + uint[16] b; + b = 7; +} +while (a) { + bool b; + b = a; +} +switch_dummy = b; +switch (switch_dummy) { + case 0 { + b = 3; + } + default { + bool b; + b = !a; + cx q[0], q[1]; + } +} +c[0] = measure q[0]; +c[1] = measure q[1]; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_parameter(self): + """We should support a `Var` clashing in name with a `Parameter` if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(1) + qc.add_var("a", False) + qc.rx(Parameter("a"), 0) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input float[64] a; +qubit[1] q; +bool a__generated0; +a__generated0 = false; +rx(a) q[0]; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_register(self): + """We should support a `Var` clashing in name with a `Register` if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c")) + qc.add_input("c", types.Bool()) + qc.add_var("q", False) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool c__generated0; +bit[2] c; +qubit[2] q; +bool q__generated1; +q__generated1 = false; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_gate(self): + """We should support a `Var` clashing in name with some gate if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(2) + qc.add_input("cx", types.Bool()) + qc.add_input("U", types.Bool()) + qc.add_var("rx", expr.lift(5, types.Uint(8))) + + qc.cx(0, 1) + qc.u(0.5, 0.125, 0.25, 0) + # We don't actually use `rx`, but it's still in the `stdgates` include. + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool cx__generated0; +input bool U__generated1; +qubit[2] q; +uint[8] rx__generated2; +rx__generated2 = 5; +cx q[0], q[1]; +U(0.5, 0.125, 0.25) q[0]; """ self.assertEqual(dumps(qc), expected) @@ -2654,3 +2804,11 @@ def test_disallow_opaque_instruction(self): QASM3ExporterError, "Exporting opaque instructions .* is not yet supported" ): exporter.dumps(qc) + + def test_disallow_export_of_inner_scope(self): + """A circuit with captures can't be a top-level OQ3 program.""" + qc = QuantumCircuit(captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex( + QASM3ExporterError, "cannot export an inner scope.*as a top-level program" + ): + dumps(qc) From cadc6f17f549fb084067c3f79f44345a6214b523 Mon Sep 17 00:00:00 2001 From: Hiroshi Horii Date: Thu, 2 May 2024 20:39:22 +0900 Subject: [PATCH 023/159] fix bugs in example of EstimatorV2 (#12326) --- qiskit/primitives/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 0a36dbbb0bd..2423f3545f8 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -86,17 +86,17 @@ estimator = Estimator() # calculate [ ] - job = estimator.run([(psi1, hamiltonian1, [theta1])]) + job = estimator.run([(psi1, H1, [theta1])]) job_result = job.result() # It will block until the job finishes. - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # calculate [ [, # ], # [] ] job2 = estimator.run( [ - (psi1, [hamiltonian1, hamiltonian3], [theta1, theta3]), - (psi2, hamiltonian2, theta2) + (psi1, [H1, H3], [theta1, theta3]), + (psi2, H2, theta2) ], precision=0.01 ) From d6c74c265ab4d5ed1d16694804d9fd7130003bb5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 2 May 2024 14:25:04 +0100 Subject: [PATCH 024/159] Add indexing and bitshift expressions (#12310) This adds the concepts of `Index` and the binary operations `SHIFT_LEFT` and `SHIFT_RIGHT` to the `Expr` system, and threads them through all the `ExprVisitor` nodes defined in Qiskit, including OQ3 and QPY serialisation/deserialisation. (The not-new OQ3 parser is managed separately and doesn't have support for _any_ `Expr` nodes at the moment). Along with `Store`, this should close the gap between what Qiskit was able to represent with dynamic circuits, and what was supported by hardware with direct OpenQASM 3 submission, although since this remains Qiskit's fairly low-level representation, it still was potentially more ergonomic to use OpenQASM 3 strings. This remains a general point for improvement in the Qiskit API, however. --- qiskit/circuit/classical/expr/__init__.py | 24 +++++- qiskit/circuit/classical/expr/constructors.py | 85 ++++++++++++++++++- qiskit/circuit/classical/expr/expr.py | 41 +++++++++ qiskit/circuit/classical/expr/visitors.py | 20 +++++ qiskit/qasm3/ast.py | 8 ++ qiskit/qasm3/exporter.py | 3 + qiskit/qasm3/printer.py | 24 ++++-- qiskit/qpy/__init__.py | 15 ++++ qiskit/qpy/binary_io/circuits.py | 18 ++-- qiskit/qpy/binary_io/schedules.py | 78 +++++++++-------- qiskit/qpy/binary_io/value.py | 51 ++++++++--- qiskit/qpy/type_keys.py | 1 + .../expr-bitshift-index-e9cfc6ea8729ef5e.yaml | 20 +++++ .../classical/test_expr_constructors.py | 54 ++++++++++++ .../circuit/classical/test_expr_helpers.py | 4 + .../circuit/classical/test_expr_properties.py | 15 ++++ .../circuit/test_circuit_load_from_qpy.py | 38 +++++++++ test/python/circuit/test_store.py | 28 ++++++ test/python/qasm3/test_export.py | 47 +++++++++- test/qpy_compat/test_qpy.py | 19 +++++ 20 files changed, 529 insertions(+), 64 deletions(-) create mode 100644 releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 9884062c5f5..c0057ca96f0 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -45,7 +45,7 @@ .. autoclass:: Var :members: var, name -Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes +Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes with associated types. .. autoclass:: Value @@ -62,6 +62,12 @@ :members: Op :member-order: bysource +Bit-like types (unsigned integers) can be indexed by integer types, represented by :class:`Index`. +The result is a single bit. The resulting expression has an associated memory location (and so can +be used as an lvalue for :class:`.Store`, etc) if the target is also an lvalue. + +.. autoclass:: Index + When constructing expressions, one must ensure that the types are valid for the operation. Attempts to construct expressions with invalid types will raise a regular Python ``TypeError``. @@ -122,6 +128,13 @@ .. autofunction:: less_equal .. autofunction:: greater .. autofunction:: greater_equal +.. autofunction:: shift_left +.. autofunction:: shift_right + +You can index into unsigned integers and bit-likes using another unsigned integer of any width. +This includes in storing operations, if the target of the index is writeable. + +.. autofunction:: index Qiskit's legacy method for specifying equality conditions for use in conditionals is to use a two-tuple of a :class:`.Clbit` or :class:`.ClassicalRegister` and an integer. This represents an @@ -174,6 +187,7 @@ "Cast", "Unary", "Binary", + "Index", "ExprVisitor", "iter_vars", "structurally_equivalent", @@ -185,6 +199,8 @@ "bit_and", "bit_or", "bit_xor", + "shift_left", + "shift_right", "logic_and", "logic_or", "equal", @@ -193,10 +209,11 @@ "less_equal", "greater", "greater_equal", + "index", "lift_legacy_condition", ] -from .expr import Expr, Var, Value, Cast, Unary, Binary +from .expr import Expr, Var, Value, Cast, Unary, Binary, Index from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue from .constructors import ( lift, @@ -214,5 +231,8 @@ less_equal, greater, greater_equal, + shift_left, + shift_right, + index, lift_legacy_condition, ) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 64a19a2aee2..de3875eef90 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -37,7 +37,7 @@ import typing -from .expr import Expr, Var, Value, Unary, Binary, Cast +from .expr import Expr, Var, Value, Unary, Binary, Cast, Index from ..types import CastKind, cast_kind from .. import types @@ -471,3 +471,86 @@ def greater_equal(left: typing.Any, right: typing.Any, /) -> Expr: Uint(3)) """ return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) + + +def _shift_like( + op: Binary.Op, left: typing.Any, right: typing.Any, type: types.Type | None +) -> Expr: + if type is not None and type.kind is not types.Uint: + raise TypeError(f"type '{type}' is not a valid bitshift operand type") + if isinstance(left, Expr): + left = _coerce_lossless(left, type) if type is not None else left + else: + left = lift(left, type) + right = lift(right) + if left.type.kind != types.Uint or right.type.kind != types.Uint: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + return Binary(op, left, right, left.type) + + +def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: + """Create a 'bitshift left' expression node from the given two values, resolving any implicit + casts and lifting the values into :class:`Value` nodes if required. + + If ``type`` is given, the ``left`` operand will be coerced to it (if possible). + + Examples: + Shift the value of a standalone variable left by some amount:: + + >>> from qiskit.circuit.classical import expr, types + >>> a = expr.Var.new("a", types.Uint(8)) + >>> expr.shift_left(a, 4) + Binary(Binary.Op.SHIFT_LEFT, \ +Var(, Uint(8), name='a'), \ +Value(4, Uint(3)), \ +Uint(8)) + + Shift an integer literal by a variable amount, coercing the type of the literal:: + + >>> expr.shift_left(3, a, types.Uint(16)) + Binary(Binary.Op.SHIFT_LEFT, \ +Value(3, Uint(16)), \ +Var(, Uint(8), name='a'), \ +Uint(16)) + """ + return _shift_like(Binary.Op.SHIFT_LEFT, left, right, type) + + +def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: + """Create a 'bitshift right' expression node from the given values, resolving any implicit casts + and lifting the values into :class:`Value` nodes if required. + + If ``type`` is given, the ``left`` operand will be coerced to it (if possible). + + Examples: + Shift the value of a classical register right by some amount:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.shift_right(ClassicalRegister(8, "a"), 4) + Binary(Binary.Op.SHIFT_RIGHT, \ +Var(ClassicalRegister(8, "a"), Uint(8)), \ +Value(4, Uint(3)), \ +Uint(8)) + """ + return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) + + +def index(target: typing.Any, index: typing.Any, /) -> Expr: + """Index into the ``target`` with the given integer ``index``, lifting the values into + :class:`Value` nodes if required. + + This can be used as the target of a :class:`.Store`, if the ``target`` is itself an lvalue. + + Examples: + Index into a classical register with a literal:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.index(ClassicalRegister(8, "a"), 3) + Index(Var(ClassicalRegister(8, "a"), Uint(8)), Value(3, Uint(2)), Bool()) + """ + target, index = lift(target), lift(index) + if target.type.kind is not types.Uint or index.type.kind is not types.Uint: + raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") + return Index(target, index, types.Bool()) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index c22870e51fe..62b6829ce4a 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -300,6 +300,11 @@ class Op(enum.Enum): The binary mathematical relations :data:`EQUAL`, :data:`NOT_EQUAL`, :data:`LESS`, :data:`LESS_EQUAL`, :data:`GREATER` and :data:`GREATER_EQUAL` take unsigned integers (with an implicit cast to make them the same width), and return a Boolean. + + The bitshift operations :data:`SHIFT_LEFT` and :data:`SHIFT_RIGHT` can take bit-like + container types (e.g. unsigned integers) as the left operand, and any integer type as the + right-hand operand. In all cases, the output bit width is the same as the input, and zeros + fill in the "exposed" spaces. """ # If adding opcodes, remember to add helper constructor functions in `constructors.py` @@ -327,6 +332,10 @@ class Op(enum.Enum): """Numeric greater than. ``lhs > rhs``.""" GREATER_EQUAL = 11 """Numeric greater than or equal to. ``lhs >= rhs``.""" + SHIFT_LEFT = 12 + """Zero-padding bitshift to the left. ``lhs << rhs``.""" + SHIFT_RIGHT = 13 + """Zero-padding bitshift to the right. ``lhs >> rhs``.""" def __str__(self): return f"Binary.{super().__str__()}" @@ -354,3 +363,35 @@ def __eq__(self, other): def __repr__(self): return f"Binary({self.op}, {self.left}, {self.right}, {self.type})" + + +@typing.final +class Index(Expr): + """An indexing expression. + + Args: + target: The object being indexed. + index: The expression doing the indexing. + type: The resolved type of the result. + """ + + __slots__ = ("target", "index") + + def __init__(self, target: Expr, index: Expr, type: types.Type): + self.target = target + self.index = index + self.type = type + + def accept(self, visitor, /): + return visitor.visit_index(self) + + def __eq__(self, other): + return ( + isinstance(other, Index) + and self.type == other.type + and self.target == other.target + and self.index == other.index + ) + + def __repr__(self): + return f"Index({self.target}, {self.index}, {self.type})" diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index c0c1a5894af..744257714b7 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -55,6 +55,9 @@ def visit_binary(self, node: expr.Binary, /) -> _T_co: # pragma: no cover def visit_cast(self, node: expr.Cast, /) -> _T_co: # pragma: no cover return self.visit_generic(node) + def visit_index(self, node: expr.Index, /) -> _T_co: # pragma: no cover + return self.visit_generic(node) + class _VarWalkerImpl(ExprVisitor[typing.Iterable[expr.Var]]): __slots__ = () @@ -75,6 +78,10 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): yield from node.operand.accept(self) + def visit_index(self, node, /): + yield from node.target.accept(self) + yield from node.index.accept(self) + _VAR_WALKER = _VarWalkerImpl() @@ -164,6 +171,16 @@ def visit_cast(self, node, /): self.other = self.other.operand return node.operand.accept(self) + def visit_index(self, node, /): + if self.other.__class__ is not node.__class__ or self.other.type != node.type: + return False + other = self.other + self.other = other.target + if not node.target.accept(self): + return False + self.other = other.index + return node.index.accept(self) + def structurally_equivalent( left: expr.Expr, @@ -235,6 +252,9 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): return False + def visit_index(self, node, /): + return node.target.accept(self) + _IS_LVALUE = _IsLValueImpl() diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 7674eace89d..300c53900d4 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -252,6 +252,8 @@ class Op(enum.Enum): GREATER_EQUAL = ">=" EQUAL = "==" NOT_EQUAL = "!=" + SHIFT_LEFT = "<<" + SHIFT_RIGHT = ">>" def __init__(self, op: Op, left: Expression, right: Expression): self.op = op @@ -265,6 +267,12 @@ def __init__(self, type: ClassicalType, operand: Expression): self.operand = operand +class Index(Expression): + def __init__(self, target: Expression, index: Expression): + self.target = target + self.index = index + + class IndexSet(ASTNode): """ A literal index set of values:: diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index b85e35e22a5..16bc2529ca7 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1165,3 +1165,6 @@ def visit_binary(self, node, /): return ast.Binary( ast.Binary.Op[node.op.name], node.left.accept(self), node.right.accept(self) ) + + def visit_index(self, node, /): + return ast.Index(node.target.accept(self), node.index.accept(self)) diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index ba253144a16..58f689c2c2e 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -34,13 +34,16 @@ # indexing and casting are all higher priority than these, so we just ignore them. _BindingPower = collections.namedtuple("_BindingPower", ("left", "right"), defaults=(255, 255)) _BINDING_POWER = { - # Power: (21, 22) + # Power: (24, 23) # - ast.Unary.Op.LOGIC_NOT: _BindingPower(right=20), - ast.Unary.Op.BIT_NOT: _BindingPower(right=20), + ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22), + ast.Unary.Op.BIT_NOT: _BindingPower(right=22), # - # Multiplication/division/modulo: (17, 18) - # Addition/subtraction: (15, 16) + # Multiplication/division/modulo: (19, 20) + # Addition/subtraction: (17, 18) + # + ast.Binary.Op.SHIFT_LEFT: _BindingPower(15, 16), + ast.Binary.Op.SHIFT_RIGHT: _BindingPower(15, 16), # ast.Binary.Op.LESS: _BindingPower(13, 14), ast.Binary.Op.LESS_EQUAL: _BindingPower(13, 14), @@ -332,6 +335,17 @@ def _visit_Cast(self, node: ast.Cast): self.visit(node.operand) self.stream.write(")") + def _visit_Index(self, node: ast.Index): + if isinstance(node.target, (ast.Unary, ast.Binary)): + self.stream.write("(") + self.visit(node.target) + self.stream.write(")") + else: + self.visit(node.target) + self.stream.write("[") + self.visit(node.index) + self.stream.write("]") + def _visit_ClassicalDeclaration(self, node: ast.ClassicalDeclaration) -> None: self._start_line() self.visit(node.type) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 7851db5c2a1..d7275bcd62f 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -382,6 +382,21 @@ Notably, this new type-code indexes into pre-defined variables from the circuit header, rather than redefining the variable again in each location it is used. + +Changes to EXPRESSION +--------------------- + +The EXPRESSION type code has a new possible entry, ``i``, corresponding to :class:`.expr.Index` +nodes. + +====================== ========= ======================================================= ======== +Qiskit class Type code Payload Children +====================== ========= ======================================================= ======== +:class:`~.expr.Index` ``i`` No additional payload. The children are the target 2 + and the index, in that order. +====================== ========= ======================================================= ======== + + .. _qpy_version_11: Version 11 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 1cf003ff358..25103e7b4c2 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -696,6 +696,7 @@ def _dumps_instruction_parameter( index_map=index_map, use_symengine=use_symengine, standalone_var_indices=standalone_var_indices, + version=version, ) return type_key, data_bytes @@ -808,6 +809,7 @@ def _write_instruction( value.write_value( file_obj, op_condition, + version=version, index_map=index_map, standalone_var_indices=standalone_var_indices, ) @@ -837,7 +839,7 @@ def _write_instruction( return custom_operations_list -def _write_pauli_evolution_gate(file_obj, evolution_gate): +def _write_pauli_evolution_gate(file_obj, evolution_gate, version): operator_list = evolution_gate.operator standalone = False if not isinstance(operator_list, list): @@ -856,7 +858,7 @@ def _write_elem(buffer, op): data = common.data_to_binary(operator, _write_elem) pauli_data_buf.write(data) - time_type, time_data = value.dumps_value(evolution_gate.time) + time_type, time_data = value.dumps_value(evolution_gate.time, version=version) time_size = len(time_data) synth_class = str(type(evolution_gate.synthesis).__name__) settings_dict = evolution_gate.synthesis.settings @@ -918,7 +920,7 @@ def _write_custom_operation( if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: has_definition = True - data = common.data_to_binary(operation, _write_pauli_evolution_gate) + data = common.data_to_binary(operation, _write_pauli_evolution_gate, version=version) size = len(data) elif type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: # For ControlledGate, we have to access and store the private `_definition` rather than the @@ -976,7 +978,7 @@ def _write_custom_operation( return new_custom_instruction -def _write_calibrations(file_obj, calibrations, metadata_serializer): +def _write_calibrations(file_obj, calibrations, metadata_serializer, version): flatten_dict = {} for gate, caldef in calibrations.items(): for (qubits, params), schedule in caldef.items(): @@ -1000,8 +1002,8 @@ def _write_calibrations(file_obj, calibrations, metadata_serializer): for qubit in qubits: file_obj.write(struct.pack("!q", qubit)) for param in params: - value.write_value(file_obj, param) - schedules.write_schedule_block(file_obj, schedule, metadata_serializer) + value.write_value(file_obj, param, version=version) + schedules.write_schedule_block(file_obj, schedule, metadata_serializer, version=version) def _write_registers(file_obj, in_circ_regs, full_bits): @@ -1211,7 +1213,7 @@ def write_circuit( metadata_size = len(metadata_raw) num_instructions = len(circuit) circuit_name = circuit.name.encode(common.ENCODE) - global_phase_type, global_phase_data = value.dumps_value(circuit.global_phase) + global_phase_type, global_phase_data = value.dumps_value(circuit.global_phase, version=version) with io.BytesIO() as reg_buf: num_qregs = _write_registers(reg_buf, circuit.qregs, circuit.qubits) @@ -1305,7 +1307,7 @@ def write_circuit( instruction_buffer.close() # Write calibrations - _write_calibrations(file_obj, circuit.calibrations, metadata_serializer) + _write_calibrations(file_obj, circuit.calibrations, metadata_serializer, version=version) _write_layout(file_obj, circuit) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index a94372f3bfc..1a3393b1e4c 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -328,7 +328,7 @@ def _read_element(file_obj, version, metadata_deserializer, use_symengine): return instance -def _loads_reference_item(type_key, data_bytes, version, metadata_deserializer): +def _loads_reference_item(type_key, data_bytes, metadata_deserializer, version): if type_key == type_keys.Value.NULL: return None if type_key == type_keys.Program.SCHEDULE_BLOCK: @@ -346,13 +346,13 @@ def _loads_reference_item(type_key, data_bytes, version, metadata_deserializer): ) -def _write_channel(file_obj, data): +def _write_channel(file_obj, data, version): type_key = type_keys.ScheduleChannel.assign(data) common.write_type_key(file_obj, type_key) - value.write_value(file_obj, data.index) + value.write_value(file_obj, data.index, version=version) -def _write_waveform(file_obj, data): +def _write_waveform(file_obj, data, version): samples_bytes = common.data_to_binary(data.samples, np.save) header = struct.pack( @@ -363,39 +363,43 @@ def _write_waveform(file_obj, data): ) file_obj.write(header) file_obj.write(samples_bytes) - value.write_value(file_obj, data.name) + value.write_value(file_obj, data.name, version=version) -def _dumps_obj(obj): +def _dumps_obj(obj, version): """Wraps `value.dumps_value` to serialize dictionary and list objects which are not supported by `value.dumps_value`. """ if isinstance(obj, dict): with BytesIO() as container: - common.write_mapping(file_obj=container, mapping=obj, serializer=_dumps_obj) + common.write_mapping( + file_obj=container, mapping=obj, serializer=_dumps_obj, version=version + ) binary_data = container.getvalue() return b"D", binary_data elif isinstance(obj, list): with BytesIO() as container: - common.write_sequence(file_obj=container, sequence=obj, serializer=_dumps_obj) + common.write_sequence( + file_obj=container, sequence=obj, serializer=_dumps_obj, version=version + ) binary_data = container.getvalue() return b"l", binary_data else: - return value.dumps_value(obj) + return value.dumps_value(obj, version=version) -def _write_kernel(file_obj, data): +def _write_kernel(file_obj, data, version): name = data.name params = data.params - common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj) - value.write_value(file_obj, name) + common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj, version=version) + value.write_value(file_obj, name, version=version) -def _write_discriminator(file_obj, data): +def _write_discriminator(file_obj, data, version): name = data.name params = data.params - common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj) - value.write_value(file_obj, name) + common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj, version=version) + value.write_value(file_obj, name, version=version) def _dumps_symbolic_expr(expr, use_symengine): @@ -410,7 +414,7 @@ def _dumps_symbolic_expr(expr, use_symengine): return zlib.compress(expr_bytes) -def _write_symbolic_pulse(file_obj, data, use_symengine): +def _write_symbolic_pulse(file_obj, data, use_symengine, version): class_name_bytes = data.__class__.__name__.encode(common.ENCODE) pulse_type_bytes = data.pulse_type.encode(common.ENCODE) envelope_bytes = _dumps_symbolic_expr(data.envelope, use_symengine) @@ -436,52 +440,51 @@ def _write_symbolic_pulse(file_obj, data, use_symengine): file_obj, mapping=data._params, serializer=value.dumps_value, + version=version, ) - value.write_value(file_obj, data.duration) - value.write_value(file_obj, data.name) + value.write_value(file_obj, data.duration, version=version) + value.write_value(file_obj, data.name, version=version) -def _write_alignment_context(file_obj, context): +def _write_alignment_context(file_obj, context, version): type_key = type_keys.ScheduleAlignment.assign(context) common.write_type_key(file_obj, type_key) common.write_sequence( - file_obj, - sequence=context._context_params, - serializer=value.dumps_value, + file_obj, sequence=context._context_params, serializer=value.dumps_value, version=version ) -def _dumps_operand(operand, use_symengine): +def _dumps_operand(operand, use_symengine, version): if isinstance(operand, library.Waveform): type_key = type_keys.ScheduleOperand.WAVEFORM - data_bytes = common.data_to_binary(operand, _write_waveform) + data_bytes = common.data_to_binary(operand, _write_waveform, version=version) elif isinstance(operand, library.SymbolicPulse): type_key = type_keys.ScheduleOperand.SYMBOLIC_PULSE data_bytes = common.data_to_binary( - operand, _write_symbolic_pulse, use_symengine=use_symengine + operand, _write_symbolic_pulse, use_symengine=use_symengine, version=version ) elif isinstance(operand, channels.Channel): type_key = type_keys.ScheduleOperand.CHANNEL - data_bytes = common.data_to_binary(operand, _write_channel) + data_bytes = common.data_to_binary(operand, _write_channel, version=version) elif isinstance(operand, str): type_key = type_keys.ScheduleOperand.OPERAND_STR data_bytes = operand.encode(common.ENCODE) elif isinstance(operand, Kernel): type_key = type_keys.ScheduleOperand.KERNEL - data_bytes = common.data_to_binary(operand, _write_kernel) + data_bytes = common.data_to_binary(operand, _write_kernel, version=version) elif isinstance(operand, Discriminator): type_key = type_keys.ScheduleOperand.DISCRIMINATOR - data_bytes = common.data_to_binary(operand, _write_discriminator) + data_bytes = common.data_to_binary(operand, _write_discriminator, version=version) else: - type_key, data_bytes = value.dumps_value(operand) + type_key, data_bytes = value.dumps_value(operand, version=version) return type_key, data_bytes -def _write_element(file_obj, element, metadata_serializer, use_symengine): +def _write_element(file_obj, element, metadata_serializer, use_symengine, version): if isinstance(element, ScheduleBlock): common.write_type_key(file_obj, type_keys.Program.SCHEDULE_BLOCK) - write_schedule_block(file_obj, element, metadata_serializer, use_symengine) + write_schedule_block(file_obj, element, metadata_serializer, use_symengine, version=version) else: type_key = type_keys.ScheduleInstruction.assign(element) common.write_type_key(file_obj, type_key) @@ -490,11 +493,12 @@ def _write_element(file_obj, element, metadata_serializer, use_symengine): sequence=element.operands, serializer=_dumps_operand, use_symengine=use_symengine, + version=version, ) - value.write_value(file_obj, element.name) + value.write_value(file_obj, element.name, version=version) -def _dumps_reference_item(schedule, metadata_serializer): +def _dumps_reference_item(schedule, metadata_serializer, version): if schedule is None: type_key = type_keys.Value.NULL data_bytes = b"" @@ -504,6 +508,7 @@ def _dumps_reference_item(schedule, metadata_serializer): obj=schedule, serializer=write_schedule_block, metadata_serializer=metadata_serializer, + version=version, ) return type_key, data_bytes @@ -576,7 +581,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symen def write_schedule_block( file_obj, block, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION -): # pylint: disable=unused-argument +): """Write a single ScheduleBlock object in the file like object. Args: @@ -610,11 +615,11 @@ def write_schedule_block( file_obj.write(block_name) file_obj.write(metadata) - _write_alignment_context(file_obj, block.alignment_context) + _write_alignment_context(file_obj, block.alignment_context, version=version) for block_elm in block._blocks: # Do not call block.blocks. This implicitly assigns references to instruction. # This breaks original reference structure. - _write_element(file_obj, block_elm, metadata_serializer, use_symengine) + _write_element(file_obj, block_elm, metadata_serializer, use_symengine, version=version) # Write references flat_key_refdict = {} @@ -627,4 +632,5 @@ def write_schedule_block( mapping=flat_key_refdict, serializer=_dumps_reference_item, metadata_serializer=metadata_serializer, + version=version, ) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index a3d7ff08813..fdad363867a 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -53,7 +53,7 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _write_parameter_expression(file_obj, obj, use_symengine): +def _write_parameter_expression(file_obj, obj, use_symengine, *, version): if use_symengine: expr_bytes = obj._symbol_expr.__reduce__()[1][0] else: @@ -81,7 +81,7 @@ def _write_parameter_expression(file_obj, obj, use_symengine): value_key = symbol_key value_data = bytes() else: - value_key, value_data = dumps_value(value, use_symengine=use_symengine) + value_key, value_data = dumps_value(value, version=version, use_symengine=use_symengine) elem_header = struct.pack( formats.PARAM_EXPR_MAP_ELEM_V3_PACK, @@ -95,12 +95,13 @@ def _write_parameter_expression(file_obj, obj, use_symengine): class _ExprWriter(expr.ExprVisitor[None]): - __slots__ = ("file_obj", "clbit_indices", "standalone_var_indices") + __slots__ = ("file_obj", "clbit_indices", "standalone_var_indices", "version") - def __init__(self, file_obj, clbit_indices, standalone_var_indices): + def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.file_obj = file_obj self.clbit_indices = clbit_indices self.standalone_var_indices = standalone_var_indices + self.version = version def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") @@ -181,19 +182,30 @@ def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) _write_expr_type(self.file_obj, node.type) self.file_obj.write( - struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) + struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) node.left.accept(self) node.right.accept(self) + def visit_index(self, node, /): + if self.version < 12: + raise exceptions.UnsupportedFeatureForVersion( + "the 'Index' expression", required=12, target=self.version + ) + self.file_obj.write(type_keys.Expression.INDEX) + _write_expr_type(self.file_obj, node.type) + node.target.accept(self) + node.index.accept(self) + def _write_expr( file_obj, node: expr.Expr, clbit_indices: collections.abc.Mapping[Clbit, int], standalone_var_indices: collections.abc.Mapping[expr.Var, int], + version: int, ): - node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices)) + node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) def _write_expr_type(file_obj, type_: types.Type): @@ -406,7 +418,13 @@ def _read_expr( _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) - raise exceptions.QpyError("Invalid classical-expression Expr key '{type_key}'") + if type_key == type_keys.Expression.INDEX: + return expr.Index( + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), + type_, + ) + raise exceptions.QpyError(f"Invalid classical-expression Expr key '{type_key}'") def _read_expr_type(file_obj) -> types.Type: @@ -494,11 +512,19 @@ def write_standalone_vars(file_obj, circuit): return out -def dumps_value(obj, *, index_map=None, use_symengine=False, standalone_var_indices=None): +def dumps_value( + obj, + *, + version, + index_map=None, + use_symengine=False, + standalone_var_indices=None, +): """Serialize input value object. Args: obj (any): Arbitrary value object to serialize. + version (int): the target QPY version for the dump. index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their integer indices. @@ -535,7 +561,7 @@ def dumps_value(obj, *, index_map=None, use_symengine=False, standalone_var_indi binary_data = common.data_to_binary(obj, _write_parameter) elif type_key == type_keys.Value.PARAMETER_EXPRESSION: binary_data = common.data_to_binary( - obj, _write_parameter_expression, use_symengine=use_symengine + obj, _write_parameter_expression, use_symengine=use_symengine, version=version ) elif type_key == type_keys.Value.EXPRESSION: clbit_indices = {} if index_map is None else index_map["c"] @@ -545,6 +571,7 @@ def dumps_value(obj, *, index_map=None, use_symengine=False, standalone_var_indi _write_expr, clbit_indices=clbit_indices, standalone_var_indices=standalone_var_indices, + version=version, ) else: raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") @@ -552,12 +579,15 @@ def dumps_value(obj, *, index_map=None, use_symengine=False, standalone_var_indi return type_key, binary_data -def write_value(file_obj, obj, *, index_map=None, use_symengine=False, standalone_var_indices=None): +def write_value( + file_obj, obj, *, version, index_map=None, use_symengine=False, standalone_var_indices=None +): """Write a value to the file like object. Args: file_obj (File): A file like object to write data. obj (any): Value to write. + version (int): the target QPY version for the dump. index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their integer indices. @@ -570,6 +600,7 @@ def write_value(file_obj, obj, *, index_map=None, use_symengine=False, standalon """ type_key, data = dumps_value( obj, + version=version, index_map=index_map, use_symengine=use_symengine, standalone_var_indices=standalone_var_indices, diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 6ec85115b55..3ff6b4a35af 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -457,6 +457,7 @@ class Expression(TypeKeyBase): CAST = b"c" UNARY = b"u" BINARY = b"b" + INDEX = b"i" @classmethod def assign(cls, obj): diff --git a/releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml b/releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml new file mode 100644 index 00000000000..78d31f8238a --- /dev/null +++ b/releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml @@ -0,0 +1,20 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + indexing and bitshifting of unsigned integers and bitlikes (e.g. :class:`.ClassicalRegister`). + For example, it is now possible to compare one register with the bitshift of another:: + + from qiskit.circuit import QuantumCircuit, ClassicalRegister + from qiskit.circuit.classical import expr + + cr1 = ClassicalRegister(4, "cr1") + cr2 = ClassicalRegister(4, "cr2") + qc = QuantumCircuit(cr1, cr2) + with qc.if_test(expr.equal(cr1, expr.shift_left(cr2, 2))): + pass + + Qiskit can also represent a condition that dynamically indexes into a register:: + + with qc.if_test(expr.index(cr1, cr2)): + pass diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index b655acf5749..10cef88122c 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -387,3 +387,57 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + + def test_index_explicit(self): + cr = ClassicalRegister(4, "c") + a = expr.Var.new("a", types.Uint(8)) + + self.assertEqual( + expr.index(cr, 3), + expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()), + ) + self.assertEqual( + expr.index(a, cr), + expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), + ) + + def test_index_forbidden(self): + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(Clbit(), 3) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), False) + + @ddt.data( + (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), + (expr.shift_right, expr.Binary.Op.SHIFT_RIGHT), + ) + @ddt.unpack + def test_shift_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + a = expr.Var.new("a", types.Uint(4)) + + self.assertEqual( + function(cr, 5), + expr.Binary( + opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + ), + ) + self.assertEqual( + function(a, cr), + expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), + ) + self.assertEqual( + function(3, 5, types.Uint(8)), + expr.Binary( + opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + ), + ) + + @ddt.data(expr.shift_left, expr.shift_right) + def test_shift_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), Clbit()) diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index f52a896df46..5264e55a52d 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -30,6 +30,8 @@ class TestStructurallyEquivalent(QiskitTestCase): expr.logic_not(Clbit()), expr.bit_and(5, ClassicalRegister(3, "a")), expr.logic_and(expr.less(2, ClassicalRegister(3, "a")), expr.lift(Clbit())), + expr.shift_left(expr.shift_right(255, 3), 3), + expr.index(expr.Var.new("a", types.Uint(8)), 0), ) def test_equivalent_to_self(self, node): self.assertTrue(expr.structurally_equivalent(node, node)) @@ -124,6 +126,7 @@ class TestIsLValue(QiskitTestCase): expr.Var.new("b", types.Uint(8)), expr.Var(Clbit(), types.Bool()), expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)), + expr.index(expr.Var.new("a", types.Uint(8)), 0), ) def test_happy_cases(self, lvalue): self.assertTrue(expr.is_lvalue(lvalue)) @@ -139,6 +142,7 @@ def test_happy_cases(self, lvalue): expr.Var.new("b", types.Bool()), types.Bool(), ), + expr.index(expr.bit_not(expr.Var.new("a", types.Uint(8))), 0), ) def test_bad_cases(self, not_an_lvalue): self.assertFalse(expr.is_lvalue(not_an_lvalue)) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 56726b3342d..6e1dcf77e1e 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -51,6 +51,21 @@ def test_types_can_be_cloned(self, obj): expr.Value(True, types.Bool()), types.Bool(), ), + expr.Index( + expr.Var.new("a", types.Uint(3)), + expr.Binary( + expr.Binary.Op.SHIFT_LEFT, + expr.Binary( + expr.Binary.Op.SHIFT_RIGHT, + expr.Var.new("b", types.Uint(3)), + expr.Value(1, types.Uint(1)), + types.Uint(3), + ), + expr.Value(1, types.Uint(1)), + types.Uint(3), + ), + types.Bool(), + ), ) def test_expr_can_be_cloned(self, obj): """Test that various ways of cloning an `Expr` object are valid and produce equal output.""" diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index efae9697f18..216ea3a59bb 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1916,6 +1916,33 @@ def test_load_empty_vars_switch(self): self.assertMinimalVarEqual(old, new) self.assertDeprecatedBitProperties(old, new) + def test_roundtrip_index_expr(self): + """Test that the `Index` node round-trips.""" + a = expr.Var.new("a", types.Uint(8)) + cr = ClassicalRegister(4, "cr") + qc = QuantumCircuit(cr, inputs=[a]) + qc.store(expr.index(cr, 0), expr.index(a, a)) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + def test_roundtrip_bitshift_expr(self): + """Test that bit-shift expressions can round-trip.""" + a = expr.Var.new("a", types.Uint(8)) + cr = ClassicalRegister(4, "cr") + qc = QuantumCircuit(cr, inputs=[a]) + with qc.if_test(expr.equal(expr.shift_right(expr.shift_left(a, 1), 1), a)): + pass + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 12)) def test_pre_v12_rejects_standalone_var(self, version): """Test that dumping to older QPY versions rejects standalone vars.""" @@ -1926,6 +1953,17 @@ def test_pre_v12_rejects_standalone_var(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 12)) + def test_pre_v12_rejects_index(self, version): + """Test that dumping to older QPY versions rejects the `Index` node.""" + # Be sure to use a register, since standalone vars would be rejected for other reasons. + qc = QuantumCircuit(ClassicalRegister(2, "cr")) + qc.store(expr.index(qc.cregs[0], 0), False) + with io.BytesIO() as fptr, self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 12 is required.*Index" + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 425eae55a4b..139192745d2 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -29,6 +29,14 @@ def test_happy_path_construction(self): self.assertEqual(constructed.lvalue, lvalue) self.assertEqual(constructed.rvalue, rvalue) + def test_store_to_index(self): + lvalue = expr.index(expr.Var.new("a", types.Uint(8)), 3) + rvalue = expr.lift(False) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, rvalue) + def test_implicit_cast(self): lvalue = expr.Var.new("a", types.Bool()) rvalue = expr.Var.new("b", types.Uint(8)) @@ -45,6 +53,11 @@ def test_rejects_non_lvalue(self): with self.assertRaisesRegex(CircuitError, "not an l-value"): Store(not_an_lvalue, rvalue) + not_an_lvalue = expr.index(expr.shift_right(expr.Var.new("a", types.Uint(8)), 1), 2) + rvalue = expr.lift(True) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + Store(not_an_lvalue, rvalue) + def test_rejects_explicit_cast(self): lvalue = expr.Var.new("a", types.Uint(16)) rvalue = expr.Var.new("b", types.Uint(8)) @@ -122,6 +135,21 @@ def test_allows_stores_with_cregs(self): actual = [instruction.operation for instruction in qc.data] self.assertEqual(actual, expected) + def test_allows_stores_with_index(self): + cr = ClassicalRegister(8, "cr") + a = expr.Var.new("a", types.Uint(3)) + qc = QuantumCircuit(cr, inputs=[a]) + qc.store(expr.index(cr, 0), False) + qc.store(expr.index(a, 3), True) + qc.store(expr.index(cr, a), expr.index(cr, 0)) + expected = [ + Store(expr.index(cr, 0), expr.lift(False)), + Store(expr.index(a, 3), expr.lift(True)), + Store(expr.index(cr, a), expr.index(cr, 0)), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + def test_lifts_values(self): a = expr.Var.new("a", types.Bool()) qc = QuantumCircuit(captures=[a]) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 8589576441a..598405beaae 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1585,11 +1585,20 @@ def test_expr_associativity_left(self): qc.if_test(expr.equal(expr.bit_and(expr.bit_and(cr1, cr2), cr3), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_or(expr.bit_or(cr1, cr2), cr3), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_xor(expr.bit_xor(cr1, cr2), cr3), 7), body.copy(), [], []) + qc.if_test( + expr.equal(expr.shift_left(expr.shift_left(cr1, cr2), cr3), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_right(expr.shift_right(cr1, cr2), cr3), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_left(expr.shift_right(cr1, cr2), cr3), 7), body.copy(), [], [] + ) qc.if_test(expr.logic_and(expr.logic_and(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) qc.if_test(expr.logic_or(expr.logic_or(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) - # Note that bitwise operations have lower priority than `==` so there's extra parentheses. - # All these operators are left-associative in OQ3. + # Note that bitwise operations except shift have lower priority than `==` so there's extra + # parentheses. All these operators are left-associative in OQ3. expected = """\ OPENQASM 3.0; include "stdgates.inc"; @@ -1602,6 +1611,12 @@ def test_expr_associativity_left(self): } if ((cr1 ^ cr2 ^ cr3) == 7) { } +if (cr1 << cr2 << cr3 == 7) { +} +if (cr1 >> cr2 >> cr3 == 7) { +} +if (cr1 >> cr2 << cr3 == 7) { +} if (cr1[0] && cr1[1] && cr1[2]) { } if (cr1[0] || cr1[1] || cr1[2]) { @@ -1621,6 +1636,15 @@ def test_expr_associativity_right(self): qc.if_test(expr.equal(expr.bit_and(cr1, expr.bit_and(cr2, cr3)), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_or(cr1, expr.bit_or(cr2, cr3)), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_xor(cr1, expr.bit_xor(cr2, cr3)), 7), body.copy(), [], []) + qc.if_test( + expr.equal(expr.shift_left(cr1, expr.shift_left(cr2, cr3)), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_right(cr1, expr.shift_right(cr2, cr3)), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_left(cr1, expr.shift_right(cr2, cr3)), 7), body.copy(), [], [] + ) qc.if_test(expr.logic_and(cr1[0], expr.logic_and(cr1[1], cr1[2])), body.copy(), [], []) qc.if_test(expr.logic_or(cr1[0], expr.logic_or(cr1[1], cr1[2])), body.copy(), [], []) @@ -1640,6 +1664,12 @@ def test_expr_associativity_right(self): } if ((cr1 ^ (cr2 ^ cr3)) == 7) { } +if (cr1 << (cr2 << cr3) == 7) { +} +if (cr1 >> (cr2 >> cr3) == 7) { +} +if (cr1 << (cr2 >> cr3) == 7) { +} if (cr1[0] && (cr1[1] && cr1[2])) { } if (cr1[0] || (cr1[1] || cr1[2])) { @@ -1709,10 +1739,21 @@ def test_expr_precedence(self): ), ) + # An extra test of the bitshifting rules, since we have to pick one or the other of + # bitshifts vs comparisons due to the typing. The first operand is inside out, the second + bitshifts = expr.equal( + expr.shift_left(expr.bit_and(expr.bit_xor(cr, cr), cr), expr.bit_or(cr, cr)), + expr.bit_or( + expr.bit_xor(expr.shift_right(cr, 3), expr.shift_left(cr, 4)), + expr.shift_left(cr, 1), + ), + ) + qc = QuantumCircuit(cr) qc.if_test(inside_out, body.copy(), [], []) qc.if_test(outside_in, body.copy(), [], []) qc.if_test(logics, body.copy(), [], []) + qc.if_test(bitshifts, body.copy(), [], []) expected = """\ OPENQASM 3.0; @@ -1726,6 +1767,8 @@ def test_expr_precedence(self): } if ((!cr[0] || !cr[0]) && !(cr[0] && cr[0]) || !(cr[0] && cr[0]) && (!cr[0] || !cr[0])) { } +if (((cr ^ cr) & cr) << (cr | cr) == (cr >> 3 ^ cr << 4 | cr << 1)) { +} """ self.assertEqual(dumps(qc), expected) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 58ee1abc2a2..3a404672946 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -802,6 +802,24 @@ def generate_standalone_var(): return [qc] +def generate_v12_expr(): + """Circuits that contain the `Index` and bitshift operators new in QPY v12.""" + import uuid + from qiskit.circuit.classical import expr, types + + a = expr.Var(uuid.UUID(bytes=b"hello, qpy world", version=4), types.Uint(8), name="a") + cr = ClassicalRegister(4, "cr") + + index = QuantumCircuit(cr, inputs=[a], name="index_expr") + index.store(expr.index(cr, 0), expr.index(a, a)) + + shift = QuantumCircuit(cr, inputs=[a], name="shift_expr") + with shift.if_test(expr.equal(expr.shift_right(expr.shift_left(a, 1), 1), a)): + pass + + return [index, shift] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -852,6 +870,7 @@ def generate_circuits(version_parts): output_circuits["annotated.qpy"] = generate_annotated_circuits() if version_parts >= (1, 1, 0): output_circuits["standalone_vars.qpy"] = generate_standalone_var() + output_circuits["v12_expr.qpy"] = generate_v12_expr() return output_circuits From c53984f10d4d814a62ce35c8b53fac7f632e1e40 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 May 2024 09:38:48 -0400 Subject: [PATCH 025/159] Add equivalence library entry for swap to ECR or CZ (#12312) * Add equivalence library entry for swap to ECR or CZ This commit adds two new equivalence library entries to cover the conversion from a SWAP gate to either ecr or cz directly. These are common 2q basis gates and without these entries in the equivalence library the path found from a lookup ends up with a much less efficient translation. This commit adds the two new entries so that the BasisTranslator will use a more efficient decomposition from the start when targeting these basis. This will hopefully result in less work for the optimization stage as the output will already be optimal and not require simplification. Testing for this PR is handled automatically by the built-in testing harness in test_gate_definitions.py that evaluates all the entries in the standard equivalence library for unitary equivalence. * Add name to annotated gate circuit in qpy backwards compat tests * Fix equivalence library tests As fallout from the addition of SingletonGate and SingletonControlledGate we were accidentally not running large portions of the unit tests which validate the default session equivalence library. This test dynamically runs based on all members of the standard gate library by looking at all defined subclasses of Gate and ControlledGate. But with the introduction of SingletonGate and SingletonControlledGate all the unparameterized gates in the library were not being run through the tests. This commit fixes this to catch that the swap definition added in the previous commit on this PR branch used an incorrect definition of SwapGate using ECRGate. The definition will be fixed in a follow up PR. * Use a more efficient and actually correct circuit for ECR target The previous definition of a swap gate using ECR rz and sx was incorrect and also not as efficient as possible. This was missed because the tests were accidently broken since #10314 which was fixed in the previous commit. This commit updates the definition to use one that is actually correct and also more efficient with fewer 1 qubit gates. Co-authored-by: Alexander Ivrii * Update ECR circuit diagram in comment * Simplify cz equivalent circuit * Simplify cz circuit even more Co-authored-by: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Co-authored-by: Alexander Ivrii --------- Co-authored-by: Alexander Ivrii Co-authored-by: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> --- .../standard_gates/equivalence_library.py | 50 +++++++++++++++++++ test/python/circuit/test_gate_definitions.py | 14 +++++- test/qpy_compat/test_qpy.py | 2 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 9793dc0202a..c4619ca2785 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -850,6 +850,56 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): def_swap.append(inst, qargs, cargs) _sel.add_equivalence(SwapGate(), def_swap) +# SwapGate +# +# q_0: ─X─ +# │ ≡ +# q_1: ─X─ +# +# ┌──────────┐┌──────┐ ┌────┐ ┌──────┐┌──────────┐┌──────┐ +# q_0: ┤ Rz(-π/2) ├┤0 ├───┤ √X ├───┤1 ├┤ Rz(-π/2) ├┤0 ├ +# └──┬────┬──┘│ Ecr │┌──┴────┴──┐│ Ecr │└──┬────┬──┘│ Ecr │ +# q_1: ───┤ √X ├───┤1 ├┤ Rz(-π/2) ├┤0 ├───┤ √X ├───┤1 ├ +# └────┘ └──────┘└──────────┘└──────┘ └────┘ └──────┘ +# +q = QuantumRegister(2, "q") +def_swap_ecr = QuantumCircuit(q) +def_swap_ecr.rz(-pi / 2, 0) +def_swap_ecr.sx(1) +def_swap_ecr.ecr(0, 1) +def_swap_ecr.rz(-pi / 2, 1) +def_swap_ecr.sx(0) +def_swap_ecr.ecr(1, 0) +def_swap_ecr.rz(-pi / 2, 0) +def_swap_ecr.sx(1) +def_swap_ecr.ecr(0, 1) +_sel.add_equivalence(SwapGate(), def_swap_ecr) + +# SwapGate +# +# q_0: ─X─ +# │ ≡ +# q_1: ─X─ +# +# global phase: 3π/2 +# ┌────┐ ┌────┐ ┌────┐ +# q_0: ┤ √X ├─■─┤ √X ├─■─┤ √X ├─■─ +# ├────┤ │ ├────┤ │ ├────┤ │ +# q_1: ┤ √X ├─■─┤ √X ├─■─┤ √X ├─■─ +# └────┘ └────┘ └────┘ +q = QuantumRegister(2, "q") +def_swap_cz = QuantumCircuit(q, global_phase=-pi / 2) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +_sel.add_equivalence(SwapGate(), def_swap_cz) + # iSwapGate # # ┌────────┐ ┌───┐┌───┐ ┌───┐ diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 8e608a16390..38bf7046cae 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -21,6 +21,7 @@ from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Operator from qiskit.circuit import ParameterVector, Gate, ControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate from qiskit.circuit.library import standard_gates from qiskit.circuit.library import ( HGate, @@ -260,7 +261,12 @@ class TestGateEquivalenceEqual(QiskitTestCase): """Test the decomposition of a gate in terms of other gates yields the same matrix as the hardcoded matrix definition.""" - class_list = Gate.__subclasses__() + ControlledGate.__subclasses__() + class_list = ( + SingletonGate.__subclasses__() + + SingletonControlledGate.__subclasses__() + + Gate.__subclasses__() + + ControlledGate.__subclasses__() + ) exclude = { "ControlledGate", "DiagonalGate", @@ -313,7 +319,11 @@ def test_equivalence_phase(self, gate_class): with self.subTest(msg=gate.name + "_" + str(ieq)): op1 = Operator(gate) op2 = Operator(equivalency) - self.assertEqual(op1, op2) + msg = ( + f"Equivalence entry from '{gate.name}' to:\n" + f"{str(equivalency.draw('text'))}\nfailed" + ) + self.assertEqual(op1, op2, msg) @ddt diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 3a404672946..f2ce2bee108 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -661,7 +661,7 @@ def generate_annotated_circuits(): CXGate(), [InverseModifier(), ControlModifier(1), PowerModifier(1.4), InverseModifier()] ) op2 = AnnotatedOperation(XGate(), InverseModifier()) - qc = QuantumCircuit(6, 1) + qc = QuantumCircuit(6, 1, name="Annotated circuits") qc.cx(0, 1) qc.append(op1, [0, 1, 2]) qc.h(4) From 469c9894d13f142921607cc74b399b70b9282384 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 2 May 2024 15:13:34 +0100 Subject: [PATCH 026/159] Support standalone `Var` throughout transpiler (#12322) * Support standalone `Var` throughout transpiler This adds the missing pieces to fully support standalone `Var` nodes through every part of the transpiler (that I could detect). It's quite possible there's some problem in a more esoteric non-preset pass somewhere, but I couldn't spot them. For the most part there were very few changes needed to the actual passes, and only one place in `QuantumCircuit` that had previously been missed. Most of the commit is updating passes to correctly pass `inline_captures=True` when appropriate for dealing with `DAGCircuit.compose`, and making sure that any place that needed to build a raw `DAGCircuit` for a rebuild _without_ using `DAGCircuit.copy_empty_like` made sure to correctly add in the variables. This commit adds specific tests for every pass that I touched, plus the general integration tests that we have for the transpiler to make sure that OQ3 and QPY serialisation work afterwards. * Clarify comment --- qiskit/circuit/quantumcircuit.py | 4 +- qiskit/circuit/store.py | 3 + .../passes/basis/basis_translator.py | 4 +- .../passes/basis/unroll_custom_definitions.py | 2 +- .../transpiler/passes/layout/apply_layout.py | 6 ++ .../transpiler/passes/layout/sabre_layout.py | 6 ++ .../passes/optimization/optimize_annotated.py | 2 +- .../passes/routing/stochastic_swap.py | 23 +++-- .../passes/synthesis/high_level_synthesis.py | 2 +- qiskit/transpiler/passes/utils/gates_basis.py | 6 +- .../python/circuit/test_circuit_operations.py | 25 +++++ test/python/compiler/test_transpiler.py | 89 ++++++++++++++++- test/python/transpiler/test_apply_layout.py | 26 +++++ .../transpiler/test_basis_translator.py | 97 ++++++++++++++++++- .../transpiler/test_gates_in_basis_pass.py | 42 ++++++++ .../transpiler/test_high_level_synthesis.py | 55 +++++++++++ .../transpiler/test_optimize_annotated.py | 24 ++++- test/python/transpiler/test_sabre_layout.py | 43 +++++++- .../python/transpiler/test_stochastic_swap.py | 44 ++++++++- .../test_unroll_custom_definitions.py | 56 ++++++++++- 20 files changed, 536 insertions(+), 23 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ad966b685e7..abd48c686b1 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -621,9 +621,7 @@ def reverse_ops(self) -> "QuantumCircuit": q_1: ┤ RX(1.57) ├───── └──────────┘ """ - reverse_circ = QuantumCircuit( - self.qubits, self.clbits, *self.qregs, *self.cregs, name=self.name + "_reverse" - ) + reverse_circ = self.copy_empty_like(self.name + "_reverse") for instruction in reversed(self.data): reverse_circ._append(instruction.replace(operation=instruction.operation.reverse_ops())) diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py index 857cb4f6c2d..6bbc5439332 100644 --- a/qiskit/circuit/store.py +++ b/qiskit/circuit/store.py @@ -59,6 +59,9 @@ class Store(Instruction): :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for subclassing.""" + # This is a compiler/backend intrinsic operation, separate to any quantum processing. + _directive = True + def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): """ Args: diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index c38d6581776..074c6d341ba 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -148,12 +148,12 @@ def run(self, dag): # Names of instructions assumed to supported by any backend. if self._target is None: - basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay"] + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] target_basis = set(self._target_basis) source_basis = set(self._extract_basis(dag)) qargs_local_source_basis = {} else: - basic_instrs = ["barrier", "snapshot"] + basic_instrs = ["barrier", "snapshot", "store"] target_basis = self._target.keys() - set(self._non_global_operations) source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 12e6811a2f0..2a95f540f88 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -61,7 +61,7 @@ def run(self, dag): return dag if self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} device_insts = basic_insts | set(self._basis_gates) for node in dag.op_nodes(): diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index c36a7e11107..e87b6f7ce4e 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -61,6 +61,12 @@ def run(self, dag): new_dag = DAGCircuit() new_dag.add_qreg(q) + for var in dag.iter_input_vars(): + new_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + new_dag.add_captured_var(var) + for var in dag.iter_declared_vars(): + new_dag.add_declared_var(var) new_dag.metadata = dag.metadata new_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 92227f3c37d..31609b87868 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -308,6 +308,12 @@ def run(self, dag): mapped_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): mapped_dag.add_creg(creg) + for var in dag.iter_input_vars(): + mapped_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + mapped_dag.add_captured_var(var) + for var in dag.iter_declared_vars(): + mapped_dag.add_declared_var(var) mapped_dag._global_phase = dag._global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index 65d06436cc5..0b9b786a07f 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -77,7 +77,7 @@ def __init__( self._top_level_only = not recurse or (self._basis_gates is None and self._target is None) if not self._top_level_only and self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit): diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 3b80bf7b31a..ec7ea814913 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -33,7 +33,6 @@ ForLoopOp, SwitchCaseOp, ControlFlowOp, - Instruction, CASE_DEFAULT, ) from qiskit._accelerate import stochastic_swap as stochastic_swap_rs @@ -266,11 +265,15 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit): # Output any swaps if best_depth > 0: logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth) - dag.compose(best_circuit, qubits={bit: bit for bit in best_circuit.qubits}) + dag.compose( + best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True + ) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer - dag.compose(layer["graph"], qubits=best_layout.reorder_bits(dag.qubits)) + dag.compose( + layer["graph"], qubits=best_layout.reorder_bits(dag.qubits), inline_captures=True + ) def _mapper(self, circuit_graph, coupling_graph, trials=20): """Map a DAGCircuit onto a CouplingMap using swap gates. @@ -438,7 +441,7 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout root_dag, self.coupling_map, layout, final_layout, seed=self._new_seed() ) if swap_dag.size(recurse=False): - updated_dag_block.compose(swap_dag, qubits=swap_qubits) + updated_dag_block.compose(swap_dag, qubits=swap_qubits, inline_captures=True) idle_qubits &= set(updated_dag_block.idle_wires()) # Now for each block, expand it to be full width over all active wires (all blocks of a @@ -504,10 +507,18 @@ def _dag_from_block(block, node, root_dag): out.add_qreg(qreg) # For clbits, we need to take more care. Nested control-flow might need registers to exist for # conditions on inner blocks. `DAGCircuit.substitute_node_with_dag` handles this register - # mapping when required, so we use that with a dummy block. + # mapping when required, so we use that with a dummy block that pretends to act on all variables + # in the DAG. out.add_clbits(node.cargs) + for var in block.iter_input_vars(): + out.add_input_var(var) + for var in block.iter_captured_vars(): + out.add_captured_var(var) + for var in block.iter_declared_vars(): + out.add_declared_var(var) + dummy = out.apply_operation_back( - Instruction("dummy", len(node.qargs), len(node.cargs), []), + IfElseOp(expr.lift(True), block.copy_empty_like(vars_mode="captures")), node.qargs, node.cargs, check=False, diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index fd21ae6a75f..150874a84c7 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -363,7 +363,7 @@ def __init__( # include path for when target exists but target.num_qubits is None (BasicSimulator) 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"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit) -> DAGCircuit: diff --git a/qiskit/transpiler/passes/utils/gates_basis.py b/qiskit/transpiler/passes/utils/gates_basis.py index 657b1d13485..b1f004cc0df 100644 --- a/qiskit/transpiler/passes/utils/gates_basis.py +++ b/qiskit/transpiler/passes/utils/gates_basis.py @@ -32,7 +32,7 @@ def __init__(self, basis_gates=None, target=None): self._basis_gates = None if basis_gates is not None: self._basis_gates = set(basis_gates).union( - {"measure", "reset", "barrier", "snapshot", "delay"} + {"measure", "reset", "barrier", "snapshot", "delay", "store"} ) self._target = target @@ -46,8 +46,8 @@ def run(self, dag): def _visit_target(dag, wire_map): for gate in dag.op_nodes(): - # Barrier is universal and supported by all backends - if gate.name == "barrier": + # Barrier and store are assumed universal and supported by all backends + if gate.name in ("barrier", "store"): continue if not self._target.instruction_supported( gate.name, tuple(wire_map[bit] for bit in gate.qargs) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 9a934d70c71..6caf194d37d 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1002,6 +1002,31 @@ def test_reverse(self): self.assertEqual(qc.reverse_ops(), expected) + def test_reverse_with_standlone_vars(self): + """Test that instruction-reversing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + with qc.if_test(a): + # We don't really comment on what should happen within control-flow operations in this + # method - it's not really defined in a non-linear CFG. This deliberately uses a body + # of length 1 (a single `Store`), so there's only one possibility. + qc.add_var(c, 12) + + expected = qc.copy_empty_like() + with expected.if_test(a): + expected.add_var(c, 12) + expected.cx(0, 1) + expected.h(0) + expected.store(b, 12) + + self.assertEqual(qc.reverse_ops(), expected) + def test_repeat(self): """Test repeating the circuit works.""" qr = QuantumRegister(2) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 943de7b932e..2a8c86b27ab 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -42,13 +42,13 @@ SwitchCaseOp, WhileLoopOp, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.annotated_operation import ( AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier, ) -from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay from qiskit.circuit.measure import Measure from qiskit.circuit.reset import Reset @@ -2175,6 +2175,38 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [3, 4]) return base + def _standalone_var_circuit(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(5, 5, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + with qc.if_test(a) as else_: + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 2) + with else_: + qc.add_var(c, 12) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cz(1, 0) + qc.cz(4, 1) + qc.store(a, False) + with qc.switch(expr.bit_and(b, 7)) as case: + with case(0): + qc.cz(0, 1) + qc.cx(1, 2) + qc.cy(2, 0) + with case(case.DEFAULT): + qc.store(b, expr.bit_and(b, 7)) + return qc + @data(0, 1, 2, 3) def test_qpy_roundtrip(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" @@ -2300,6 +2332,46 @@ def test_qpy_roundtrip_control_flow_expr_backendv2(self, optimization_level): round_tripped = qpy.load(buffer)[0] self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=7) + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + basis_gates=backend.operation_names + + ["if_else", "for_loop", "while_loop", "switch_case"], + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var_target(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=11) + backend.target.add_instruction(IfElseOp, name="if_else") + backend.target.add_instruction(ForLoopOp, name="for_loop") + backend.target.add_instruction(WhileLoopOp, name="while_loop") + backend.target.add_instruction(SwitchCaseOp, name="switch_case") + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + @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.""" @@ -2350,6 +2422,21 @@ def test_qasm3_output_control_flow_expr(self, optimization_level): str, ) + @data(0, 1, 2, 3) + def test_qasm3_output_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow and standalone `Var` nodes + can be dumped into OpenQASM 3.""" + transpiled = transpile( + self._standalone_var_circuit(), + backend=GenericBackendV2(num_qubits=13, control_flow=True), + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + # 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), str) + @data(0, 1, 2, 3) def test_transpile_target_no_measurement_error(self, opt_level): """Test that transpile with a target which contains ideal measurement works diff --git a/test/python/transpiler/test_apply_layout.py b/test/python/transpiler/test_apply_layout.py index bd119c010f0..b92cc710095 100644 --- a/test/python/transpiler/test_apply_layout.py +++ b/test/python/transpiler/test_apply_layout.py @@ -15,6 +15,7 @@ import unittest from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes import ApplyLayout, SetLayout @@ -167,6 +168,31 @@ def test_final_layout_is_updated(self): ), ) + def test_works_with_var_nodes(self): + """Test that standalone var nodes work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + expected = QuantumCircuit(QuantumRegister(2, "q"), *qc.cregs, inputs=[a]) + expected.add_var(b, 12) + expected.h(1) + expected.cx(1, 0) + expected.measure([1, 0], [0, 1]) + expected.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + pass_ = ApplyLayout() + pass_.property_set["layout"] = Layout(dict(enumerate(reversed(qc.qubits)))) + after = pass_(qc) + + self.assertEqual(after, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 218cd8162d5..24e5e68ba98 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -19,8 +19,10 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit import transpile -from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit +from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit, Measure +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( + HGate, U1Gate, U2Gate, U3Gate, @@ -889,6 +891,50 @@ def test_unrolling_parameterized_composite_gates(self): self.assertEqual(circuit_to_dag(expected), out_dag) + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with no target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the basis set. + out = BasisTranslator(eq_lib, ["my_h", "my_cx"])(qc) + self.assertEqual(out, expected) + class TestBasisExamples(QiskitTestCase): """Test example circuits targeting example bases over the StandardEquivalenceLibrary.""" @@ -1127,3 +1173,52 @@ def test_2q_with_non_global_1q(self): expected.sx(1) expected.rz(3 * pi, 1) self.assertEqual(output, expected) + + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with a target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the target. + target = Target() + target.add_instruction(MyHGate(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(Measure(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(MyCXGate(), {(0, 1): None, (1, 0): None}) + + out = BasisTranslator(eq_lib, {"my_h", "my_cx"}, target)(qc) + self.assertEqual(out, expected) diff --git a/test/python/transpiler/test_gates_in_basis_pass.py b/test/python/transpiler/test_gates_in_basis_pass.py index 2138070ed9d..06ce5e0f670 100644 --- a/test/python/transpiler/test_gates_in_basis_pass.py +++ b/test/python/transpiler/test_gates_in_basis_pass.py @@ -13,6 +13,7 @@ """Test GatesInBasis pass.""" from qiskit.circuit import QuantumCircuit, ForLoopOp, IfElseOp, SwitchCaseOp, Clbit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import HGate, CXGate, UGate, XGate, ZGate from qiskit.circuit.measure import Measure from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary @@ -269,3 +270,44 @@ def test_basis_gates_target(self): pass_ = GatesInBasis(target=complete) pass_(circuit) self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_basis_gates(self): + """Test that `Store` is treated as an automatic built-in when given basis gates.""" + pass_ = GatesInBasis(basis_gates=["h", "cx"]) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_target(self): + """Test that `Store` is treated as an automatic built-in when given a target.""" + target = Target() + target.add_instruction(HGate(), {(0,): None, (1,): None}) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + pass_ = GatesInBasis(target=target) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 0f074865f41..9a2432b82f9 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -28,6 +28,7 @@ Operation, EquivalenceLibrary, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( SwapGate, CXGate, @@ -36,6 +37,7 @@ U3Gate, U2Gate, U1Gate, + UGate, CU3Gate, CU1Gate, ) @@ -2042,6 +2044,59 @@ def test_unroll_empty_definition_with_phase(self): expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + def test_leave_store_alone_basis(self): + """Don't attempt to synthesise `Store` instructions with basis gates.""" + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, basis_gates=["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to synthesise `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_optimize_annotated.py b/test/python/transpiler/test_optimize_annotated.py index 6a506516d19..5e573b551dd 100644 --- a/test/python/transpiler/test_optimize_annotated.py +++ b/test/python/transpiler/test_optimize_annotated.py @@ -13,7 +13,8 @@ """Test OptimizeAnnotated pass""" from qiskit.circuit import QuantumCircuit, Gate -from qiskit.circuit.library import SwapGate, CXGate +from qiskit.circuit.classical import expr, types +from qiskit.circuit.library import SwapGate, CXGate, HGate from qiskit.circuit.annotated_operation import ( AnnotatedOperation, ControlModifier, @@ -193,3 +194,24 @@ def test_if_else(self): ) self.assertEqual(qc_optimized, expected_qc) + + def test_standalone_var(self): + """Test that standalone vars work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(3, 3, inputs=[a]) + qc.add_var(b, 12) + qc.append(AnnotatedOperation(HGate(), [ControlModifier(1), ControlModifier(1)]), [0, 1, 2]) + qc.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc.measure([0, 1, 2], [0, 1, 2]) + qc.store(a, expr.logic_and(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(HGate().control(2, annotated=True), [0, 1, 2]) + expected.cx(0, 1) + expected.measure([0, 1, 2], [0, 1, 2]) + expected.store(a, expr.logic_and(expected.clbits[0], expected.clbits[1])) + + self.assertEqual(OptimizeAnnotated()(qc), expected) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7640149e039..487fbf9daef 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -17,9 +17,10 @@ import math from qiskit import QuantumRegister, QuantumCircuit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import EfficientSU2 from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager -from qiskit.transpiler.passes import SabreLayout, DenseLayout +from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.compiler.transpiler import transpile @@ -257,6 +258,46 @@ def test_layout_many_search_trials(self): [layout[q] for q in qc.qubits], [22, 7, 2, 12, 1, 5, 14, 4, 11, 0, 16, 15, 3, 10] ) + def test_support_var_with_rust_fastpath(self): + """Test that the joint layout/embed/routing logic for the Rust-space fast-path works in the + presence of standalone `Var` nodes.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 0) + + out = SabreLayout(CouplingMap.from_line(8), seed=0, swap_trials=2, layout_trials=2)(qc) + + self.assertIsInstance(out, QuantumCircuit) + self.assertEqual(out.layout.initial_index_layout(), [4, 5, 6, 3, 2, 0, 1, 7]) + + def test_support_var_with_explicit_routing_pass(self): + """Test that the logic works if an explicit routing pass is given.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 0) + + cm = CouplingMap.from_line(8) + pass_ = SabreLayout( + cm, seed=0, routing_pass=StochasticSwap(cm, trials=1, seed=0, fake_run=True) + ) + _ = pass_(qc) + layout = pass_.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5]) + class DensePartialSabreTrial(AnalysisPass): """Pass to run dense layout as a sabre trial.""" diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index fb27076d03d..df5948ed715 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -27,7 +27,7 @@ from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 from qiskit.compiler.transpiler import transpile from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order from test.utils._canonical import canonicalize_control_flow # pylint: disable=wrong-import-order @@ -897,6 +897,48 @@ def test_if_else_expr(self): check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_standalone_vars(self): + """Test that the routing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(3, 2) + qc.cx(3, 0) + qc.cx(4, 2) + qc.cx(4, 0) + qc.cx(1, 4) + qc.cx(3, 4) + with qc.if_test(a): + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with qc.if_test(a) as else_: + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with else_: + qc.cx(1, 2) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cx(1, 3) + qc.store(a, False) + with qc.switch(b) as case: + with case(0): + qc.add_var(c, 12) + qc.cx(3, 1) + with case(case.DEFAULT): + qc.cx(3, 1) + + cm = CouplingMap.from_line(5) + pm = PassManager([StochasticSwap(cm, seed=0), CheckMap(cm)]) + _ = pm.run(qc) + self.assertTrue(pm.property_set["is_swap_mapped"]) + def test_no_layout_change(self): """test controlflow with no layout change needed""" num_qubits = 5 diff --git a/test/python/transpiler/test_unroll_custom_definitions.py b/test/python/transpiler/test_unroll_custom_definitions.py index cfed023795d..5bd16f027e4 100644 --- a/test/python/transpiler/test_unroll_custom_definitions.py +++ b/test/python/transpiler/test_unroll_custom_definitions.py @@ -16,10 +16,11 @@ from qiskit.circuit import EquivalenceLibrary, Gate, Qubit, Clbit, Parameter from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.transpiler import Target -from qiskit.circuit.library import CXGate, U3Gate +from qiskit.circuit.library import CXGate, U3Gate, UGate from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -317,3 +318,56 @@ def test_unroll_empty_definition_with_phase(self): pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u"]) expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone(self): + """Don't attempt to unroll `Store` instructions.""" + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to unroll `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) From 122c64e758caa8b1febacce2f41a3a9e1684adc5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 May 2024 10:13:48 -0400 Subject: [PATCH 027/159] Add ElidePermutations transpiler pass (#9523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ElideSwaps transpiler pass This commit adds a new transpiler pass ElideSwaps which is a logical optimization pass designed to run prior to layout or any other physical embedding steps in the transpiler pipeline. It traverses the DAG in topological order and when a swap gate is encountered it removes that gate and instead permutes the logical qubits for any subsequent gates in the DAG. This will eliminate any swaps in a circuit not caused by routing. Additionally, this pass is added to the preset pass manager for optimization level 3, we can consider adding it to other levels too if we think it makes sense (there is little overhead, and almost 0 if there are no swaps). One thing to note is that this pass does not recurse into control flow blocks at all, it treats them as black box operations. So if any control flow blocks contain swap gates those will not be optimized away. This change was made because mapping the permutation outside and inside any control flow block was larger in scope than what the intent for this pass was. That type of work is better suited for the routing passes which already have to reason about this. * Update tests with optimization level 3 * Pass final layout from ElideSwap to output The new ElideSwap pass is causing an output permutation just as a routing pass would. This information needs to be passed through to the output in case there is no layout or routing run. In those cases the information about the output permutation caused by the swap elision will be lost and doing layout aware operations like Operator.from_circuit() will not be able to reason about the permutation. This commit fixes this by inserting the original layout and qubit mapping into the property set along with the final layout. Then the base pass class and pass manager class are updated to use the original layout as the initial layout if one isn't set. In cases where we run layout and routing the output metadata from those passes will superscede these new metadata fields. * Move pass in opt level 3 earlier in stage and skip with explicit layout This commit fixes 2 issues in the execution of the new ElideSwaps pass as part of optimization level 3. First we were running it too late in the process both after high level synthesis (which isn't relavant yet, but will be soon when this is expanded to elide all permutations not just swaps) and also after reset diagonal before measurement. The second issue is that if the user is specifying to run with a manual layout set we should not run this pass, as it will interfere with the user intent. * Doc and copy paste fixes * Expand test coverage * Update permutation tracking There were 2 issues with the permutation tracking done in an earlier commit. First, it conflicted with the final_layout property set via routing (or internally by the routing done in the combined sabre layout). This was breaking conditional checks in the preset pass manager around embedding. To fix this a different property is used and set as the output final layout if no final layout is set. The second issue was the output layout object was not taking into account a set initial layout which will permute the qubits and cause the output to not be up to date. This is fixed by updating apply layout to apply the initial layout to the elision_final_layout in the property set. * Generalize pass to support PermutationGate too This commit generalizes the pass from just considering swap gates to all permutations (via the PermutationGate class). This enables the pass to elide additional circuit permutations, not just the special case of a swap gate. The pass and module are renamed accordingly to ElidePermutations and elide_permutations respectively. * Fix permutation handling This commit fixes the recently added handling of the PermutationGate so that it correctly is mapping the qubits. The previous iteration of this messed up the mapping logic so it wasn't valid. * Fix formatting * Fix final layout handling for no initial layout * Improve documentation and log a warning if run post layout * Fix final layout handling with no ElideSwaps being run * Fix docs build * Fix release note * Fix typo * Add test for routing and elide permutations * Apply suggestions from code review Co-authored-by: Jim Garrison * Rename test file to test_elide_permutations.py * Apply suggestions from code review Co-authored-by: Kevin Hartman * Fix test import after rebase * fixing failing test cases this should pass CI after merging #12057 * addresses kehas comments - thx * Adding FinalyzeLayouts pass to pull the virtual circuit permutation from ElidePermutations to the final layout * formatting * partial rebase on top of 12057 + tests * also need test_operator for partial rebase * removing from transpiler flow for now; reworking elide tests * also adding permutation gate to the example * also temporarily reverting test_transpiler.py * update to release notes * minor fixes * Apply suggestions from code review * Fix lint * Update qiskit/transpiler/passes/optimization/elide_permutations.py * Add test to test we skip after layout * Integrate FinalizeLayouts into the PassManager harness This commit integrates the function that finalize layouts was performing into the passmanager harnesss. We'll always need to run the equivalent of finalize layout if any passes are setting a virtual permutation so using a standalone pass that can be forgotten is potentially error prone. This inlines the logic as part of the passmanager's output preparation stage so we always finalize the layout. * Compose a potential existing virtual_permutation_layout * Remove unused import --------- Co-authored-by: Jim Garrison Co-authored-by: Kevin Hartman Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Co-authored-by: AlexanderIvrii Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/transpiler/passes/__init__.py | 2 + .../transpiler/passes/layout/apply_layout.py | 1 - qiskit/transpiler/passes/layout/set_layout.py | 2 +- .../passes/optimization/__init__.py | 1 + .../passes/optimization/elide_permutations.py | 112 +++++ qiskit/transpiler/passmanager.py | 45 +- .../preset_passmanagers/builtin_plugins.py | 1 - .../add-elide-swaps-b0a4c373c9af1efd.yaml | 41 ++ .../transpiler/test_elide_permutations.py | 430 ++++++++++++++++++ 9 files changed, 631 insertions(+), 4 deletions(-) create mode 100644 qiskit/transpiler/passes/optimization/elide_permutations.py create mode 100644 releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml create mode 100644 test/python/transpiler/test_elide_permutations.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index b2614624b41..c1e1705f1cc 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -87,6 +87,7 @@ EchoRZXWeylDecomposition ResetAfterMeasureSimplification OptimizeCliffords + ElidePermutations NormalizeRXAngle OptimizeAnnotated @@ -236,6 +237,7 @@ from .optimization import CollectCliffords from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords +from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index e87b6f7ce4e..9cbedcef5ab 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -56,7 +56,6 @@ def run(self, dag): raise TranspilerError("The 'layout' must be full (with ancilla).") post_layout = self.property_set["post_layout"] - q = QuantumRegister(len(layout), "q") new_dag = DAGCircuit() diff --git a/qiskit/transpiler/passes/layout/set_layout.py b/qiskit/transpiler/passes/layout/set_layout.py index cfdc6d630df..c4e5faa91fb 100644 --- a/qiskit/transpiler/passes/layout/set_layout.py +++ b/qiskit/transpiler/passes/layout/set_layout.py @@ -63,7 +63,7 @@ def run(self, dag): layout = None else: raise InvalidLayoutError( - f"SetLayout was intialized with the layout type: {type(self.layout)}" + f"SetLayout was initialized with the layout type: {type(self.layout)}" ) self.property_set["layout"] = layout return dag diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 40e877ec514..082cb3f67ec 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -35,5 +35,6 @@ from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords from .collect_cliffords import CollectCliffords +from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated diff --git a/qiskit/transpiler/passes/optimization/elide_permutations.py b/qiskit/transpiler/passes/optimization/elide_permutations.py new file mode 100644 index 00000000000..d9704ff0518 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/elide_permutations.py @@ -0,0 +1,112 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +"""Remove any swap gates in the circuit by pushing it through into a qubit permutation.""" + +import logging + +from qiskit.circuit.library.standard_gates import SwapGate +from qiskit.circuit.library.generalized_gates import PermutationGate +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.layout import Layout + +logger = logging.getLogger(__name__) + + +class ElidePermutations(TransformationPass): + r"""Remove permutation operations from a pre-layout circuit + + This pass is intended to be run before a layout (mapping virtual qubits + to physical qubits) is set during the transpilation pipeline. This + pass iterates over the :class:`~.DAGCircuit` and when a :class:`~.SwapGate` + or :class:`~.PermutationGate` are encountered it permutes the virtual qubits in + the circuit and removes the swap gate. This will effectively remove any + :class:`~SwapGate`\s or :class:`~PermutationGate` in the circuit prior to running + layout. If this pass is run after a layout has been set it will become a no-op + (and log a warning) as this optimization is not sound after physical qubits are + selected and there are connectivity constraints to adhere to. + + For tracking purposes this pass sets 3 values in the property set if there + are any :class:`~.SwapGate` or :class:`~.PermutationGate` objects in the circuit + and the pass isn't a no-op. + + * ``original_layout``: The trivial :class:`~.Layout` for the input to this pass being run + * ``original_qubit_indices``: The mapping of qubit objects to positional indices for the state + of the circuit as input to this pass. + * ``virtual_permutation_layout``: A :class:`~.Layout` object mapping input qubits to the output + state after eliding permutations. + + These three properties are needed for the transpiler to track the permutations in the out + :attr:`.QuantumCircuit.layout` attribute. The elision of permutations is equivalent to a + ``final_layout`` set by routing and all three of these attributes are needed in the case + """ + + def run(self, dag): + """Run the ElidePermutations pass on ``dag``. + + Args: + dag (DAGCircuit): the DAG to be optimized. + + Returns: + DAGCircuit: the optimized DAG. + """ + if self.property_set["layout"] is not None: + logger.warning( + "ElidePermutations is not valid after a layout has been set. This indicates " + "an invalid pass manager construction." + ) + return dag + + op_count = dag.count_ops(recurse=False) + if op_count.get("swap", 0) == 0 and op_count.get("permutation", 0) == 0: + return dag + + new_dag = dag.copy_empty_like() + qubit_mapping = list(range(len(dag.qubits))) + + def _apply_mapping(qargs): + return tuple(dag.qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) + + for node in dag.topological_op_nodes(): + if not isinstance(node.op, (SwapGate, PermutationGate)): + new_dag.apply_operation_back( + node.op, _apply_mapping(node.qargs), node.cargs, check=False + ) + elif getattr(node.op, "condition", None) is not None: + new_dag.apply_operation_back( + node.op, _apply_mapping(node.qargs), node.cargs, check=False + ) + elif isinstance(node.op, SwapGate): + index_0 = dag.find_bit(node.qargs[0]).index + index_1 = dag.find_bit(node.qargs[1]).index + qubit_mapping[index_1], qubit_mapping[index_0] = ( + qubit_mapping[index_0], + qubit_mapping[index_1], + ) + elif isinstance(node.op, PermutationGate): + starting_indices = [qubit_mapping[dag.find_bit(qarg).index] for qarg in node.qargs] + pattern = node.op.params[0] + pattern_indices = [qubit_mapping[idx] for idx in pattern] + for i, j in zip(starting_indices, pattern_indices): + qubit_mapping[i] = j + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + + new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) + if current_layout := self.property_set["virtual_permutation_layout"] is not None: + self.property_set["virtual_permutation_layout"] = current_layout.compose(new_layout) + else: + self.property_set["virtual_permutation_layout"] = new_layout + return new_dag diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 025c3ea9dfd..bb1344e34cb 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -29,7 +29,7 @@ from qiskit.passmanager.exceptions import PassManagerError from .basepasses import BasePass from .exceptions import TranspilerError -from .layout import TranspileLayout +from .layout import TranspileLayout, Layout _CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] @@ -69,6 +69,7 @@ def _passmanager_backend( ) -> QuantumCircuit: out_program = dag_to_circuit(passmanager_ir, copy_operations=False) + self._finalize_layouts(passmanager_ir) out_name = kwargs.get("output_name", None) if out_name is not None: out_program.name = out_name @@ -96,6 +97,48 @@ def _passmanager_backend( return out_program + def _finalize_layouts(self, dag): + if (virtual_permutation_layout := self.property_set["virtual_permutation_layout"]) is None: + return + + self.property_set.pop("virtual_permutation_layout") + + # virtual_permutation_layout is usually created before extending the layout with ancillas, + # so we extend the permutation to be identity on ancilla qubits + original_qubit_indices = self.property_set.get("original_qubit_indices", None) + for oq in original_qubit_indices: + if oq not in virtual_permutation_layout: + virtual_permutation_layout[oq] = original_qubit_indices[oq] + + t_qubits = dag.qubits + + if (t_initial_layout := self.property_set.get("layout", None)) is None: + t_initial_layout = Layout(dict(enumerate(t_qubits))) + + if (t_final_layout := self.property_set.get("final_layout", None)) is None: + t_final_layout = Layout(dict(enumerate(t_qubits))) + + # Ordered list of original qubits + original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()} + original_qubits = [] + for i in range(len(original_qubits_reverse)): + original_qubits.append(original_qubits_reverse[i]) + + virtual_permutation_layout_inv = virtual_permutation_layout.inverse( + original_qubits, original_qubits + ) + + t_initial_layout_inv = t_initial_layout.inverse(original_qubits, t_qubits) + + # ToDo: this can possibly be made simpler + new_final_layout = t_initial_layout_inv + new_final_layout = new_final_layout.compose(virtual_permutation_layout_inv, original_qubits) + new_final_layout = new_final_layout.compose(t_initial_layout, original_qubits) + new_final_layout = new_final_layout.compose(t_final_layout, t_qubits) + + self.property_set["layout"] = t_initial_layout + self.property_set["final_layout"] = new_final_layout + def append( self, passes: Task | list[Task], diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index fc01f6eface..8eddb9adf63 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -154,7 +154,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml b/releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml new file mode 100644 index 00000000000..a8da2921990 --- /dev/null +++ b/releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml @@ -0,0 +1,41 @@ +--- +features: + - | + Added a new optimization transpiler pass, :class:`~.ElidePermutations`, + which is designed to run prior to the :ref:`layout_stage` and will + optimize away any :class:`~.SwapGate`\s and + :class:`~qiskit.circuit.library.PermutationGate`\s + in a circuit by permuting virtual + qubits. For example, taking a circuit with :class:`~.SwapGate`\s: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 1) + qc.swap(2, 0) + qc.cx(1, 0) + qc.measure_all() + qc.draw("mpl") + + will remove the swaps when the pass is run: + + .. plot:: + :include-source: + + from qiskit.transpiler.passes import ElidePermutations + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 1) + qc.swap(2, 0) + qc.cx(1, 0) + qc.measure_all() + + ElidePermutations()(qc).draw("mpl") + + The pass also sets the ``virtual_permutation_layout`` property set, storing + the permutation of the virtual qubits that was optimized away. diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py new file mode 100644 index 00000000000..d3d807834fc --- /dev/null +++ b/test/python/transpiler/test_elide_permutations.py @@ -0,0 +1,430 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test ElidePermutations pass""" + +import unittest + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.library.generalized_gates import PermutationGate +from qiskit.transpiler.passes.optimization.elide_permutations import ElidePermutations +from qiskit.circuit.controlflow import IfElseOp +from qiskit.quantum_info import Operator +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestElidePermutations(QiskitTestCase): + """Test elide permutations logical optimization pass.""" + + def setUp(self): + super().setUp() + self.swap_pass = ElidePermutations() + + def test_no_swap(self): + """Test no swap means no transform.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_swap_in_middle(self): + """Test swap in middle of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(0, 1) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(0, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_at_beginning(self): + """Test swap in beginning of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.swap(0, 1) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(1) + expected.cx(0, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_at_end(self): + """Test swap at the end of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + qc.swap(0, 1) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_multiple_swaps(self): + """Test quantum circuit with multiple swaps.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + expected = QuantumCircuit(3) + expected.h(0) + expected.cx(2, 1) + expected.h(2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_before_measure(self): + """Test swap before measure is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.swap(0, 1) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_if_else_block(self): + """Test swap elision only happens outside control flow.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + with qc.if_test((0, 0)): + qc.swap(0, 1) + qc.cx(0, 1) + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_swap_if_else_block_with_outside_swap(self): + """Test swap elision only happens outside control flow.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(2, 0) + body = QuantumCircuit(2) + body.swap(0, 1) + if_else_op = IfElseOp((qc.clbits[0], 0), body) + + qc.append(if_else_op, [0, 1]) + qc.cx(0, 1) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.append(IfElseOp((expected.clbits[0], 0), body), [2, 1]) + expected.cx(2, 1) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_condition(self): + """Test swap elision doesn't touch conditioned swap.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(0, 1).c_if(qc.clbits[0], 0) + qc.cx(0, 1) + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_permutation_in_middle(self): + """Test permutation in middle of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 0) + expected.barrier(0, 1, 2) + expected.measure(2, 0) + expected.measure(1, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_at_beginning(self): + """Test permutation in beginning of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(2) + expected.cx(1, 0) + expected.barrier(0, 1, 2) + expected.measure(2, 0) + expected.measure(1, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_at_end(self): + """Test permutation at end of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_and_permutation(self): + """Test a combination of swap and permutation gates.""" + qc = QuantumCircuit(3, 3) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.swap(0, 2) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_before_measure(self): + """Test permutation before measure is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2]) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(2, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + +class TestElidePermutationsInTranspileFlow(QiskitTestCase): + """ + Test elide permutations in the full transpile pipeline, especially that + "layout" and "final_layout" attributes are updated correctly + as to preserve unitary equivalence. + """ + + def test_not_run_after_layout(self): + """Test ElidePermutations doesn't do anything after layout.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + spm = generate_preset_pass_manager( + optimization_level=3, initial_layout=list(range(2, -1, -1)), seed_transpiler=42 + ) + spm.layout += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + self.assertIn("swap", res.count_ops()) + self.assertTrue(res.layout.final_index_layout(), [0, 1, 2]) + + def test_unitary_equivalence(self): + """Test unitary equivalence of the original and transpiled circuits.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + with self.subTest("no coupling map"): + spm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, seed_transpiler=42, coupling_map=CouplingMap.from_line(3) + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + def test_unitary_equivalence_routing_and_basis_translation(self): + """Test on a larger example that includes routing and basis translation.""" + + qc = QuantumCircuit(5) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.append(PermutationGate([0, 2, 1]), [0, 1, 2]) + qc.h(1) + + with self.subTest("no coupling map"): + spm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + coupling_map=CouplingMap.from_line(5), + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("no coupling map but with initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + initial_layout=[4, 2, 1, 3, 0], + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("coupling map and initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + initial_layout=[4, 2, 1, 3, 0], + basis_gates=["u", "cz"], + coupling_map=CouplingMap.from_line(5), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("larger coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=42, + coupling_map=CouplingMap.from_line(8), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + + qc_with_ancillas = QuantumCircuit(8) + qc_with_ancillas.append(qc, [0, 1, 2, 3, 4]) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc_with_ancillas))) + + with self.subTest("larger coupling map and initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=42, + initial_layout=[4, 2, 7, 3, 6], + coupling_map=CouplingMap.from_line(8), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + + qc_with_ancillas = QuantumCircuit(8) + qc_with_ancillas.append(qc, [0, 1, 2, 3, 4]) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc_with_ancillas))) + + +if __name__ == "__main__": + unittest.main() From 8ba79743ee022c118e3d0dd78d91ecb7cc629b71 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 2 May 2024 10:19:32 -0400 Subject: [PATCH 028/159] Update make_data_bin() to return DataBin proper (#12219) * Update make_data_bin() to return DataBin proper * Remove metaclass I checked all of our repos and it's not used by anything * fix implementations * remove make_data_bin usage * make the DataBin follow the Shaped protocol * add it back to containers module (removed by accident) * get implemntatinos to assign shape * improve the repr * add reno * fix reno formatting * improve error string * use views instead of sequences * fix tests --------- Co-authored-by: Takashi Imamichi --- qiskit/primitives/backend_estimator_v2.py | 5 +- qiskit/primitives/backend_sampler_v2.py | 9 +- qiskit/primitives/base/base_estimator.py | 13 +- qiskit/primitives/containers/data_bin.py | 185 +++++++++++------- qiskit/primitives/statevector_estimator.py | 8 +- qiskit/primitives/statevector_sampler.py | 9 +- ...databin-construction-72ec041075410cb2.yaml | 16 ++ .../primitives/containers/test_data_bin.py | 74 ++++--- .../containers/test_primitive_result.py | 11 +- .../primitives/containers/test_pub_result.py | 12 +- .../primitives/test_backend_sampler_v2.py | 5 +- .../primitives/test_statevector_sampler.py | 5 +- 12 files changed, 190 insertions(+), 162 deletions(-) create mode 100644 releasenotes/notes/databin-construction-72ec041075410cb2.yaml diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 9afc6d892f3..d94d3674e9a 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -31,7 +31,7 @@ from .backend_estimator import _pauli_expval_with_variance, _prepare_counts, _run_circuits from .base import BaseEstimatorV2 -from .containers import EstimatorPubLike, PrimitiveResult, PubResult +from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.bindings_array import BindingsArray from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob @@ -256,8 +256,7 @@ def _postprocess_pub( evs[index] += expval * coeff variances[index] += variance * coeff**2 stds = np.sqrt(variances / shots) - data_bin_cls = self._make_data_bin(pub) - data_bin = data_bin_cls(evs=evs, stds=stds) + data_bin = DataBin(evs=evs, stds=stds, shape=evs.shape) return PubResult(data_bin, metadata={"target_precision": pub.precision}) def _bind_and_add_measurements( diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index 87507e1d54d..3b560645af6 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -27,10 +27,10 @@ from qiskit.primitives.base import BaseSamplerV2 from qiskit.primitives.containers import ( BitArray, + DataBin, PrimitiveResult, PubResult, SamplerPubLike, - make_data_bin, ) from qiskit.primitives.containers.bit_array import _min_num_bytes from qiskit.primitives.containers.sampler_pub import SamplerPub @@ -210,15 +210,10 @@ def _postprocess_pub( ary = _samples_to_packed_array(samples, item.num_bits, item.start) arrays[item.creg_name][index] = ary - data_bin_cls = make_data_bin( - [(item.creg_name, BitArray) for item in meas_info], - shape=shape, - ) meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - data_bin = data_bin_cls(**meas) - return PubResult(data_bin, metadata={}) + return PubResult(DataBin(**meas, shape=shape), metadata={}) def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 47c0ba10bf0..9191b4162d9 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -18,8 +18,6 @@ from collections.abc import Iterable, Sequence from copy import copy from typing import Generic, TypeVar -import numpy as np -from numpy.typing import NDArray from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job @@ -27,7 +25,6 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from ..containers import ( - make_data_bin, DataBin, EstimatorPubLike, PrimitiveResult, @@ -205,12 +202,10 @@ class BaseEstimatorV2(ABC): """ @staticmethod - def _make_data_bin(pub: EstimatorPub) -> DataBin: - # provide a standard way to construct estimator databins to ensure that names match - # across implementations - return make_data_bin( - (("evs", NDArray[np.float64]), ("stds", NDArray[np.float64])), pub.shape - ) + def _make_data_bin(_: EstimatorPub) -> type[DataBin]: + # this method is present for backwards compat. new primitive implementatinos + # should avoid it. + return DataBin @abstractmethod def run( diff --git a/qiskit/primitives/containers/data_bin.py b/qiskit/primitives/containers/data_bin.py index 50934b6cdfd..5ea31f7510e 100644 --- a/qiskit/primitives/containers/data_bin.py +++ b/qiskit/primitives/containers/data_bin.py @@ -15,110 +15,151 @@ """ from __future__ import annotations -from collections.abc import Iterable, Sequence -from dataclasses import make_dataclass -from typing import Any +from typing import Any, ItemsView, Iterable, KeysView, ValuesView +import numpy as np -class DataBinMeta(type): - """Metaclass for :class:`DataBin` that adds the shape to the type name. +from .shape import ShapedMixin, ShapeInput, shape_tuple - This is so that the class has a custom repr with DataBin<*shape> notation. - """ - def __repr__(cls): - name = cls.__name__ - if cls._SHAPE is None: - return name - shape = ",".join(map(str, cls._SHAPE)) - return f"{name}<{shape}>" +def _value_repr(value: Any) -> str: + """Helper function for :meth:`DataBin.__repr__`.""" + if isinstance(value, np.ndarray): + return f"np.ndarray()" + return repr(value) + + +class DataBin(ShapedMixin): + """Namespace for storing data. + + .. code-block:: python + data = DataBin( + alpha=BitArray.from_bitstrings(["0010"]), + beta=np.array([1.2]) + ) -class DataBin(metaclass=DataBinMeta): - """Base class for data bin containers. + print("alpha data:", data.alpha) + print("beta data:", data.beta) - Subclasses are typically made via :class:`~make_data_bin`, which is a specialization of - :class:`make_dataclass`. """ - _RESTRICTED_NAMES = { - "_RESTRICTED_NAMES", - "_SHAPE", - "_FIELDS", - "_FIELD_TYPES", - "keys", - "values", - "items", - } - _SHAPE: tuple[int, ...] | None = None - _FIELDS: tuple[str, ...] = () - """The fields allowed in this data bin.""" - _FIELD_TYPES: tuple[type, ...] = () - """The types of each field.""" + __slots__ = ("_data", "_shape") + + _RESTRICTED_NAMES = frozenset( + { + "_RESTRICTED_NAMES", + "_SHAPE", + "_FIELDS", + "_FIELD_TYPES", + "_data", + "_shape", + "keys", + "values", + "items", + "shape", + "ndim", + "size", + } + ) + + def __init__(self, *, shape: ShapeInput = (), **data): + """ + Args: + data: Name/value data to place in the data bin. + shape: The leading shape common to all entries in the data bin. This defaults to + the trivial leading shape of ``()`` that is compatible with all objects. + + Raises: + ValueError: If a name overlaps with a method name on this class. + ValueError: If some value is inconsistent with the provided shape. + """ + if not self._RESTRICTED_NAMES.isdisjoint(data): + bad_names = sorted(self._RESTRICTED_NAMES.intersection(data)) + raise ValueError(f"Cannot assign with these field names: {bad_names}") + + _setattr = super().__setattr__ + _setattr("_shape", shape_tuple(shape)) + _setattr("_data", data) + + ndim = len(self._shape) + for name, value in data.items(): + if getattr(value, "shape", shape)[:ndim] != shape: + raise ValueError(f"The value of '{name}' does not lead with the shape {shape}.") + _setattr(name, value) + + super().__init__() def __len__(self): - return len(self._FIELDS) + return len(self._data) + + def __setattr__(self, *_): + raise NotImplementedError def __repr__(self): - vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) - return f"{type(self)}({', '.join(vals)})" + vals = [f"{name}={_value_repr(val)}" for name, val in self.items()] + if self.ndim: + vals.append(f"shape={self.shape}") + return f"{type(self).__name__}({', '.join(vals)})" def __getitem__(self, key: str) -> Any: - if key not in self._FIELDS: - raise KeyError(f"Key ({key}) does not exist in this data bin.") - return getattr(self, key) + try: + return self._data[key] + except KeyError as ex: + raise KeyError(f"Key ({key}) does not exist in this data bin.") from ex def __contains__(self, key: str) -> bool: - return key in self._FIELDS + return key in self._data def __iter__(self) -> Iterable[str]: - return iter(self._FIELDS) + return iter(self._data) - def keys(self) -> Sequence[str]: - """Return a list of field names.""" - return tuple(self._FIELDS) + def keys(self) -> KeysView[str]: + """Return a view of field names.""" + return self._data.keys() - def values(self) -> Sequence[Any]: - """Return a list of values.""" - return tuple(getattr(self, key) for key in self._FIELDS) + def values(self) -> ValuesView[Any]: + """Return a view of values.""" + return self._data.values() - def items(self) -> Sequence[tuple[str, Any]]: - """Return a list of field names and values""" - return tuple((key, getattr(self, key)) for key in self._FIELDS) + def items(self) -> ItemsView[str, Any]: + """Return a view of field names and values""" + return self._data.items() + # The following properties exist to provide support to legacy private class attributes which + # gained widespread prior to qiskit 1.1. These properties will be removed once the internal + # projects have made the appropriate changes. + @property + def _FIELDS(self) -> tuple[str, ...]: # pylint: disable=invalid-name + return tuple(self._data) + + @property + def _FIELD_TYPES(self) -> tuple[Any, ...]: # pylint: disable=invalid-name + return tuple(map(type, self.values())) + + @property + def _SHAPE(self) -> tuple[int, ...]: # pylint: disable=invalid-name + return self.shape + + +# pylint: disable=unused-argument def make_data_bin( fields: Iterable[tuple[str, type]], shape: tuple[int, ...] | None = None ) -> type[DataBin]: - """Return a new subclass of :class:`~DataBin` with the provided fields and shape. + """Return the :class:`~DataBin` type. - .. code-block:: python - - my_bin = make_data_bin([("alpha", np.NDArray[np.float64])], shape=(20, 30)) - - # behaves like a dataclass - my_bin(alpha=np.empty((20, 30))) + .. note:: + This class used to return a subclass of :class:`~DataBin`. However, that caused confusion + and didn't have a useful purpose. Several internal projects made use of this internal + function prior to qiskit 1.1. This function will be removed once these internal projects + have made the appropriate changes. Args: fields: Tuples ``(name, type)`` specifying the attributes of the returned class. shape: The intended shape of every attribute of this class. Returns: - A new class. + The :class:`DataBin` type. """ - field_names, field_types = zip(*fields) if fields else ((), ()) - for name in field_names: - if name in DataBin._RESTRICTED_NAMES: - raise ValueError(f"'{name}' is a restricted name for a DataBin.") - cls = make_dataclass( - "DataBin", - dict(zip(field_names, field_types)), - bases=(DataBin,), - frozen=True, - unsafe_hash=True, - repr=False, - ) - cls._SHAPE = shape - cls._FIELDS = field_names - cls._FIELD_TYPES = field_types - return cls + return DataBin diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index a3b2cddefdd..a5dc029edf7 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -22,7 +22,7 @@ from qiskit.quantum_info import SparsePauliOp, Statevector from .base import BaseEstimatorV2 -from .containers import EstimatorPubLike, PrimitiveResult, PubResult +from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob from .utils import bound_circuit_to_instruction @@ -160,6 +160,6 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: raise ValueError("Given operator is not Hermitian and noise cannot be added.") expectation_value = rng.normal(expectation_value, precision) evs[index] = expectation_value - data_bin_cls = self._make_data_bin(pub) - data_bin = data_bin_cls(evs=evs, stds=stds) - return PubResult(data_bin, metadata={"precision": precision}) + + data = DataBin(evs=evs, stds=stds, shape=evs.shape) + return PubResult(data, metadata={"precision": precision}) diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index d04eb3894ff..c78865aec7e 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -30,10 +30,10 @@ from .base.validation import _has_measure from .containers import ( BitArray, + DataBin, PrimitiveResult, PubResult, SamplerPubLike, - make_data_bin, ) from .containers.sampler_pub import SamplerPub from .containers.bit_array import _min_num_bytes @@ -194,15 +194,10 @@ def _run_pub(self, pub: SamplerPub) -> PubResult: ary = _samples_to_packed_array(samples_array, item.num_bits, item.qreg_indices) arrays[item.creg_name][index] = ary - data_bin_cls = make_data_bin( - [(item.creg_name, BitArray) for item in meas_info], - shape=bound_circuits.shape, - ) meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - data_bin = data_bin_cls(**meas) - return PubResult(data_bin, metadata={"shots": pub.shots}) + return PubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots}) def _preprocess_circuit(circuit: QuantumCircuit): diff --git a/releasenotes/notes/databin-construction-72ec041075410cb2.yaml b/releasenotes/notes/databin-construction-72ec041075410cb2.yaml new file mode 100644 index 00000000000..a9dbd60298f --- /dev/null +++ b/releasenotes/notes/databin-construction-72ec041075410cb2.yaml @@ -0,0 +1,16 @@ +--- +features_primitives: + - | + `qiskit.primitives.containers.DataBin` now satisfies the `qiskit.primitives.containers.Shaped` + protocol. This means that every `DataBin` instance now has the additional attributes + * `shape: tuple[int, ...]` the leading shape of every entry in the instance + * `ndim: int` the length of `shape` + * `size: int` the product of the entries of `shape` + The shape can be passed to the constructor. +upgrade_primitives: + - | + The function `qiskit.primitives.containers.make_data_bin()` no longer creates and returns a + `qiskit.primitives.containers.DataBin` subclass. It instead always returns the `DataBin` class. + However, it continues to exist for backwards compatibility, though will eventually be deprecated. + All users should migrate to construct `DataBin` instances directly, instead of instantiating + subclasses as output by `make_data_bin()`. diff --git a/test/python/primitives/containers/test_data_bin.py b/test/python/primitives/containers/test_data_bin.py index b750174b7b6..a8f802ebaeb 100644 --- a/test/python/primitives/containers/test_data_bin.py +++ b/test/python/primitives/containers/test_data_bin.py @@ -15,65 +15,61 @@ import numpy as np -import numpy.typing as npt -from qiskit.primitives.containers import make_data_bin -from qiskit.primitives.containers.data_bin import DataBin, DataBinMeta +from qiskit.primitives.containers.data_bin import DataBin from test import QiskitTestCase # pylint: disable=wrong-import-order class DataBinTestCase(QiskitTestCase): """Test the DataBin class.""" - def test_make_databin(self): - """Test the make_databin() function.""" - data_bin_cls = make_data_bin( - [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) - ) - - self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) - self.assertTrue(issubclass(data_bin_cls, DataBin)) - self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) - self.assertEqual(data_bin_cls._FIELD_TYPES, (npt.NDArray[np.uint16], np.ndarray)) + def test_make_databin_no_fields(self): + """Test DataBin when no fields are given.""" + data_bin = DataBin() + self.assertEqual(len(data_bin), 0) + self.assertEqual(data_bin.shape, ()) + def test_data_bin_basic(self): + """Test DataBin function basic access.""" alpha = np.empty((10, 20), dtype=np.uint16) beta = np.empty((10, 20), dtype=int) - my_bin = data_bin_cls(alpha, beta) + my_bin = DataBin(alpha=alpha, beta=beta) + self.assertEqual(len(my_bin), 2) self.assertTrue(np.all(my_bin.alpha == alpha)) self.assertTrue(np.all(my_bin.beta == beta)) self.assertTrue("alpha=" in str(my_bin)) - self.assertTrue(str(my_bin).startswith("DataBin<10,20>")) + self.assertTrue(str(my_bin).startswith("DataBin")) + self.assertEqual(my_bin._FIELDS, ("alpha", "beta")) + self.assertEqual(my_bin._FIELD_TYPES, (np.ndarray, np.ndarray)) - my_bin = data_bin_cls(beta=beta, alpha=alpha) + my_bin = DataBin(beta=beta, alpha=alpha) self.assertTrue(np.all(my_bin.alpha == alpha)) self.assertTrue(np.all(my_bin.beta == beta)) - def test_make_databin_no_shape(self): - """Test the make_databin() function with no shape.""" - data_bin_cls = make_data_bin([("alpha", dict), ("beta", int)]) + def test_constructor_failures(self): + """Test that the constructor fails when expected.""" - self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) - self.assertTrue(issubclass(data_bin_cls, DataBin)) - self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) - self.assertEqual(data_bin_cls._FIELD_TYPES, (dict, int)) + with self.assertRaisesRegex(ValueError, "Cannot assign with these field names"): + DataBin(values=6) - my_bin = data_bin_cls({1: 2}, 5) - self.assertEqual(my_bin.alpha, {1: 2}) - self.assertEqual(my_bin.beta, 5) - self.assertTrue("alpha=" in str(my_bin)) - self.assertTrue(">" not in str(my_bin)) + with self.assertRaisesRegex(ValueError, "does not lead with the shape"): + DataBin(x=np.empty((5,)), shape=(1,)) - def test_make_databin_no_fields(self): - """Test the make_data_bin() function when no fields are given.""" - data_bin_cls = make_data_bin([]) - data_bin = data_bin_cls() - self.assertEqual(len(data_bin), 0) + with self.assertRaisesRegex(ValueError, "does not lead with the shape"): + DataBin(x=np.empty((5, 2, 3)), shape=(5, 2, 3, 4)) + + def test_shape(self): + """Test shape setting and attributes.""" + databin = DataBin(x=6, y=np.empty((2, 3))) + self.assertEqual(databin.shape, ()) + + databin = DataBin(x=np.empty((5, 2)), y=np.empty((5, 2, 6)), shape=(5, 2)) + self.assertEqual(databin.shape, (5, 2)) def test_make_databin_mapping(self): - """Test the make_data_bin() function with mapping features.""" - data_bin_cls = make_data_bin([("alpha", int), ("beta", dict)]) - data_bin = data_bin_cls(10, {1: 2}) + """Test DataBin with mapping features.""" + data_bin = DataBin(alpha=10, beta={1: 2}) self.assertEqual(len(data_bin), 2) with self.subTest("iterator"): @@ -86,14 +82,14 @@ def test_make_databin_mapping(self): _ = next(iterator) with self.subTest("keys"): - lst = data_bin.keys() + lst = list(data_bin.keys()) key = lst[0] self.assertEqual(key, "alpha") key = lst[1] self.assertEqual(key, "beta") with self.subTest("values"): - lst = data_bin.values() + lst = list(data_bin.values()) val = lst[0] self.assertIsInstance(val, int) self.assertEqual(val, 10) @@ -102,7 +98,7 @@ def test_make_databin_mapping(self): self.assertEqual(val, {1: 2}) with self.subTest("items"): - lst = data_bin.items() + lst = list(data_bin.items()) key, val = lst[0] self.assertEqual(key, "alpha") self.assertIsInstance(val, int) diff --git a/test/python/primitives/containers/test_primitive_result.py b/test/python/primitives/containers/test_primitive_result.py index 93e563379c2..fc8c774a164 100644 --- a/test/python/primitives/containers/test_primitive_result.py +++ b/test/python/primitives/containers/test_primitive_result.py @@ -14,9 +14,8 @@ """Unit tests for PrimitiveResult.""" import numpy as np -import numpy.typing as npt -from qiskit.primitives.containers import PrimitiveResult, PubResult, make_data_bin +from qiskit.primitives.containers import DataBin, PrimitiveResult, PubResult from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -25,16 +24,12 @@ class PrimitiveResultCase(QiskitTestCase): def test_primitive_result(self): """Test the PrimitiveResult class.""" - data_bin_cls = make_data_bin( - [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) - ) - alpha = np.empty((10, 20), dtype=np.uint16) beta = np.empty((10, 20), dtype=int) pub_results = [ - PubResult(data_bin_cls(alpha, beta)), - PubResult(data_bin_cls(alpha, beta)), + PubResult(DataBin(alpha=alpha, beta=beta, shape=(10, 20))), + PubResult(DataBin(alpha=alpha, beta=beta, shape=(10, 20))), ] result = PrimitiveResult(pub_results, {"x": 2}) diff --git a/test/python/primitives/containers/test_pub_result.py b/test/python/primitives/containers/test_pub_result.py index 1849b77c475..011110df3bd 100644 --- a/test/python/primitives/containers/test_pub_result.py +++ b/test/python/primitives/containers/test_pub_result.py @@ -13,7 +13,7 @@ """Unit tests for PubResult.""" -from qiskit.primitives.containers import PubResult, make_data_bin +from qiskit.primitives.containers import DataBin, PubResult from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -22,13 +22,12 @@ class PubResultCase(QiskitTestCase): def test_construction(self): """Test that the constructor works.""" - data_bin = make_data_bin((("a", float), ("b", int))) - pub_result = PubResult(data_bin(a=1.0, b=2)) + pub_result = PubResult(DataBin(a=1.0, b=2)) self.assertEqual(pub_result.data.a, 1.0) self.assertEqual(pub_result.data.b, 2) self.assertEqual(pub_result.metadata, {}) - pub_result = PubResult(data_bin(a=1.0, b=2), {"x": 1}) + pub_result = PubResult(DataBin(a=1.0, b=2), {"x": 1}) self.assertEqual(pub_result.data.a, 1.0) self.assertEqual(pub_result.data.b, 2) self.assertEqual(pub_result.metadata, {"x": 1}) @@ -38,6 +37,5 @@ def test_repr(self): # we are primarily interested in making sure some future change doesn't cause the repr to # raise an error. it is more sensible for humans to detect a deficiency in the formatting # itself, should one be uncovered - data_bin = make_data_bin((("a", float), ("b", int))) - self.assertTrue(repr(PubResult(data_bin(a=1.0, b=2))).startswith("PubResult")) - self.assertTrue(repr(PubResult(data_bin(a=1.0, b=2), {"x": 1})).startswith("PubResult")) + self.assertTrue(repr(PubResult(DataBin(a=1.0, b=2))).startswith("PubResult")) + self.assertTrue(repr(PubResult(DataBin(a=1.0, b=2), {"x": 1})).startswith("PubResult")) diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index dd58920689a..9f6c007b1d5 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -15,7 +15,6 @@ from __future__ import annotations import unittest -from dataclasses import astuple from test import QiskitTestCase, combine import numpy as np @@ -604,7 +603,7 @@ def test_circuit_with_multiple_cregs(self, backend): result = sampler.run([qc], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + 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])) @@ -640,7 +639,7 @@ def test_circuit_with_aliased_cregs(self, backend): result = sampler.run([qc2], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + self.assertEqual(len(data), 3) for creg_name in target: self.assertTrue(hasattr(data, creg_name)) self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index 1a8ed0402e5..de17b282407 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -15,7 +15,6 @@ from __future__ import annotations import unittest -from dataclasses import astuple import numpy as np from numpy.typing import NDArray @@ -573,7 +572,7 @@ def test_circuit_with_multiple_cregs(self): result = sampler.run([qc], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + 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])) @@ -606,7 +605,7 @@ def test_circuit_with_aliased_cregs(self): result = sampler.run([qc2], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + self.assertEqual(len(data), 3) for creg_name in target: self.assertTrue(hasattr(data, creg_name)) self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) From c974c4fb08a14b5303ff05340530035117e03a01 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Thu, 2 May 2024 19:14:05 +0400 Subject: [PATCH 029/159] Add enhancement to BitArray (#12158) * Add transpose to BitArray * add concatenate * add marginalize and `__getitem__` * add expectation_value * add tests of concatenate * add tests for __getitem__ * add tests for marginalize * add tests for expectation_value * make concatenate static method and add stack_shots * add stack_shots and stack_bits * Apply suggestions from code review Co-authored-by: Ian Hincks * fix lint * fix transpose to allow n ints * Apply suggestions from code review Co-authored-by: Ian Hincks * Apply suggestions from code review Co-authored-by: Ian Hincks * renamed expectation_value method with expectation_values to return all expectation values * update expectation_values to allow observable array * fix lint * Update qiskit/primitives/containers/bit_array.py Co-authored-by: Ian Hincks * updated expectation_values and added slice_shots * reorg tests and add some more cases * add reno and improved an error message * Update qiskit/primitives/containers/bit_array.py Co-authored-by: Ian Hincks * fix a test --------- Co-authored-by: Ian Hincks --- qiskit/primitives/containers/bit_array.py | 294 ++++++++++++- ...d-bitarray-utilities-c85261138d5a1a97.yaml | 84 ++++ .../primitives/containers/test_bit_array.py | 415 +++++++++++++++++- 3 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index 308e51f782b..24d52ca4e85 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -19,13 +19,15 @@ from collections import defaultdict from functools import partial from itertools import chain, repeat -from typing import Callable, Iterable, Literal, Mapping +from typing import Callable, Iterable, Literal, Mapping, Sequence import numpy as np from numpy.typing import NDArray -from qiskit.result import Counts +from qiskit.exceptions import QiskitError +from qiskit.result import Counts, sampled_expectation_value +from .observables_array import ObservablesArray, ObservablesArrayLike from .shape import ShapedMixin, ShapeInput, shape_tuple # this lookup table tells you how many bits are 1 in each uint8 value @@ -37,6 +39,23 @@ def _min_num_bytes(num_bits: int) -> int: return num_bits // 8 + (num_bits % 8 > 0) +def _unpack(bit_array: BitArray) -> NDArray[np.uint8]: + arr = np.unpackbits(bit_array.array, axis=-1, bitorder="big") + arr = arr[..., -1 : -bit_array.num_bits - 1 : -1] + return arr + + +def _pack(arr: NDArray[np.uint8]) -> tuple[NDArray[np.uint8], int]: + arr = arr[..., ::-1] + num_bits = arr.shape[-1] + pad_size = -num_bits % 8 + if pad_size > 0: + pad_width = [(0, 0)] * (arr.ndim - 1) + [(pad_size, 0)] + arr = np.pad(arr, pad_width, constant_values=0) + arr = np.packbits(arr, axis=-1, bitorder="big") + return arr, num_bits + + class BitArray(ShapedMixin): """Stores an array of bit values. @@ -110,6 +129,13 @@ def __repr__(self): desc = f"" 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." + ) + return BitArray(self._array[indices], self.num_bits) + @property def array(self) -> NDArray[np.uint8]: """The raw NumPy array of data.""" @@ -347,3 +373,267 @@ def reshape(self, *shape: ShapeInput) -> "BitArray": else: raise ValueError("Cannot change the size of the array.") return BitArray(self._array.reshape(shape), self.num_bits) + + def transpose(self, *axes) -> "BitArray": + """Return a bit array with axes transposed. + + Args: + axes: None, tuple of ints or n ints. See `ndarray.transpose + `_ + for the details. + + Returns: + BitArray: A bit array with axes permuted. + + Raises: + ValueError: If ``axes`` don't match this bit array. + ValueError: If ``axes`` includes any indices that are out of bounds. + """ + if len(axes) == 0: + axes = tuple(reversed(range(self.ndim))) + if len(axes) == 1 and isinstance(axes[0], Sequence): + axes = axes[0] + if len(axes) != self.ndim: + raise ValueError("axes don't match bit array") + for i in axes: + if i >= self.ndim or self.ndim + i < 0: + raise ValueError( + f"axis {i} is out of bounds for bit array of dimension {self.ndim}." + ) + axes = tuple(i if i >= 0 else self.ndim + i for i in axes) + (-2, -1) + return BitArray(self._array.transpose(axes), self.num_bits) + + def slice_bits(self, indices: int | Sequence[int]) -> "BitArray": + """Return a bit array sliced along the bit axis of some indices of interest. + + .. note:: + + The convention used by this method is that the index ``0`` corresponds to + the least-significant bit in the :attr:`~array`, or equivalently + the right-most bitstring entry as returned by + :meth:`~get_counts` or :meth:`~get_bitstrings`, etc. + + If this bit array was produced by a sampler, then an index ``i`` corresponds to the + :class:`~.ClassicalRegister` location ``creg[i]``. + + Args: + indices: The bit positions of interest to slice along. + + Returns: + A bit array sliced along the bit axis. + + Raises: + ValueError: 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( + 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 + # unpacking. This could be fixed using bitwise functions, at the expense of a + # more complicated implementation. + arr = _unpack(self) + arr = arr[..., indices] + arr, num_bits = _pack(arr) + return BitArray(arr, num_bits) + + def slice_shots(self, indices: int | Sequence[int]) -> "BitArray": + """Return a bit array sliced along the shots axis of some indices of interest. + + Args: + indices: The shots positions of interest to slice along. + + Returns: + A bit array sliced along the shots axis. + + Raises: + ValueError: 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( + f"index {index} is out of bounds for the number of shots {self.num_shots}." + ) + arr = self._array + arr = arr[..., indices, :] + return BitArray(arr, 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. + + .. note:: + + This method returns the real part of the expectation value even if + the operator has complex coefficients due to the specification of + :func:`~.sampled_expectation_value`. + + Args: + observables: The observable(s) to take the expectation value of. + Must have a shape broadcastable with with this bit array and + the same number of qubits as the number of bits of this bit array. + The observables must be diagonal (I, Z, 0 or 1) too. + + Returns: + An array of expectation values whose shape is the broadcast shape of ``observables`` + and this bit array. + + Raises: + ValueError: If the provided observables does not have a shape broadcastable with + this bit array. + ValueError: If the provided observables does not have the same number of qubits as + the number of bits of this bit array. + ValueError: If the provided observables are not diagonal. + """ + observables = ObservablesArray.coerce(observables) + arr_indices = np.fromiter(np.ndindex(self.shape), dtype=object).reshape(self.shape) + bc_indices, bc_obs = np.broadcast_arrays(arr_indices, observables) + counts = {} + arr = np.zeros_like(bc_indices, dtype=float) + for index in np.ndindex(bc_indices.shape): + loc = bc_indices[index] + for pauli, coeff in bc_obs[index].items(): + if loc not in counts: + counts[loc] = self.get_counts(loc) + try: + expval = sampled_expectation_value(counts[loc], pauli) + except QiskitError as ex: + raise ValueError(ex.message) from ex + arr[index] += expval * coeff + return arr + + @staticmethod + def concatenate(bit_arrays: Sequence[BitArray], axis: int = 0) -> BitArray: + """Join a sequence of bit arrays along an existing axis. + + Args: + bit_arrays: The bit arrays must have (1) the same number of bits, + (2) the same number of shots, and + (3) the same shape, except in the dimension corresponding to axis + (the first, by default). + axis: The axis along which the arrays will be joined. Default is 0. + + Returns: + The concatenated bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of bits. + ValueError: If any bit arrays has a different number of shots. + ValueError: If any bit arrays has a different number of dimensions. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to concatenate") + num_bits = bit_arrays[0].num_bits + num_shots = bit_arrays[0].num_shots + ndim = bit_arrays[0].ndim + if ndim == 0: + raise ValueError("Zero-dimensional bit arrays cannot be concatenated") + for i, ba in enumerate(bit_arrays): + if ba.num_bits != num_bits: + raise ValueError( + "All bit arrays must have same number of bits, " + f"but the bit array at index 0 has {num_bits} bits " + f"and the bit array at index {i} has {ba.num_bits} bits." + ) + if ba.num_shots != num_shots: + raise ValueError( + "All bit arrays must have same number of shots, " + f"but the bit array at index 0 has {num_shots} shots " + f"and the bit array at index {i} has {ba.num_shots} shots." + ) + if ba.ndim != ndim: + raise ValueError( + "All bit arrays must have same number of dimensions, " + f"but the bit array at index 0 has {ndim} dimension(s) " + f"and the bit array at index {i} has {ba.ndim} dimension(s)." + ) + if axis < 0 or axis >= ndim: + raise ValueError(f"axis {axis} is out of bounds for bit array of dimension {ndim}.") + data = np.concatenate([ba.array for ba in bit_arrays], axis=axis) + return BitArray(data, num_bits) + + @staticmethod + def concatenate_shots(bit_arrays: Sequence[BitArray]) -> BitArray: + """Join a sequence of bit arrays along the shots axis. + + Args: + bit_arrays: The bit arrays must have (1) the same number of bits, + and (2) the same shape. + + Returns: + The stacked bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of bits. + ValueError: If any bit arrays has a different shape. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to stack") + num_bits = bit_arrays[0].num_bits + shape = bit_arrays[0].shape + for i, ba in enumerate(bit_arrays): + if ba.num_bits != num_bits: + raise ValueError( + "All bit arrays must have same number of bits, " + f"but the bit array at index 0 has {num_bits} bits " + f"and the bit array at index {i} has {ba.num_bits} bits." + ) + if ba.shape != shape: + raise ValueError( + "All bit arrays must have same shape, " + f"but the bit array at index 0 has shape {shape} " + f"and the bit array at index {i} has shape {ba.shape}." + ) + data = np.concatenate([ba.array for ba in bit_arrays], axis=-2) + return BitArray(data, num_bits) + + @staticmethod + def concatenate_bits(bit_arrays: Sequence[BitArray]) -> BitArray: + """Join a sequence of bit arrays along the bits axis. + + .. note:: + This method is equivalent to per-shot bitstring concatenation. + + Args: + bit_arrays: Bit arrays that have (1) the same number of shots, + and (2) the same shape. + + Returns: + The stacked bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of shots. + ValueError: If any bit arrays has a different shape. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to stack") + num_shots = bit_arrays[0].num_shots + shape = bit_arrays[0].shape + for i, ba in enumerate(bit_arrays): + if ba.num_shots != num_shots: + raise ValueError( + "All bit arrays must have same number of shots, " + f"but the bit array at index 0 has {num_shots} shots " + f"and the bit array at index {i} has {ba.num_shots} shots." + ) + if ba.shape != shape: + raise ValueError( + "All bit arrays must have same shape, " + f"but the bit array at index 0 has shape {shape} " + f"and the bit array at index {i} has shape {ba.shape}." + ) + # This implementation introduces a temporary 8x memory overhead due to bit + # unpacking. This could be fixed using bitwise functions, at the expense of a + # more complicated implementation. + data = np.concatenate([_unpack(ba) for ba in bit_arrays], axis=-1) + data, num_bits = _pack(data) + return BitArray(data, num_bits) diff --git a/releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml b/releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml new file mode 100644 index 00000000000..089a7bcd113 --- /dev/null +++ b/releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml @@ -0,0 +1,84 @@ +--- +features_primitives: + - | + Added methods to join multiple :class:`~.BitArray` objects along various axes. + + - :meth:`~.BitArray.concatenate`: join arrays along an existing axis of the arrays. + - :meth:`~.BitArray.concatenate_bits`: join arrays along the bit axis. + - :meth:`~.BitArray.concatenate_shots`: join arrays along the shots axis. + + .. code-block:: + + ba = BitArray.from_samples(['00', '11']) + print(ba) + # BitArray() + + # reshape the bit array because `concatenate` requires an axis. + ba_ = ba.reshape(1, 2) + print(ba_) + # BitArray() + + ba2 = BitArray.concatenate([ba_, ba_]) + print(ba2.get_bitstrings()) + # ['00', '11', '00', '11'] + + # `concatenate_bits` and `concatenates_shots` do not require any axis. + + ba3 = BitArray.concatenate_bits([ba, ba]) + print(ba3.get_bitstrings()) + # ['0000', '1111'] + + ba4 = BitArray.concatenate_shots([ba, ba]) + print(ba4.get_bitstrings()) + # ['00', '11', '00', '11'] + + - | + Added methods to generate a subset of :class:`~.BitArray` object by slicing along various axes. + + - :meth:`~.BitArray.__getitem__`: slice the array along an existing axis of the array. + - :meth:`~.BitArray.slice_bits`: slice the array along the bit axis. + - :meth:`~.BitArray.slice_shots`: slice the array along the shot axis. + + .. code-block:: + + ba = BitArray.from_samples(['0000', '0001', '0010', '0011'], 4) + print(ba) + # BitArray() + print(ba.get_bitstrings()) + # ['0000', '0001', '0010', '0011'] + + ba2 = ba.reshape(2, 2) + print(ba2) + # BitArray() + print(ba2[0].get_bitstrings()) + # ['0000', '0001'] + print(ba2[1].get_bitstrings()) + # ['0010', '0011'] + + ba3 = ba.slice_bits([0, 2]) + print(ba3.get_bitstrings()) + # ['00', '01', '00', '01'] + + ba4 = ba.slice_shots([0, 2]) + print(ba3.get_bitstrings()) + # ['0000', '0010'] + + - | + Added a method :meth:`~.BitArray.transpose` to transpose a :class:`~.BitArray`. + + .. code-block:: + + ba = BitArray.from_samples(['00', '11']).reshape(2, 1, 1) + print(ba) + # BitArray() + print(ba.transpose()) + # BitArray() + + - | + Added a method :meth:`~.BitArray.expectation_values` to compute expectation values of diagonal operators. + + .. code-block:: + + ba = BitArray.from_samples(['01', '11']) + print(ba.expectation_values(["IZ", "ZI", "01"])) + # [-1. 0. 0.5] diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index a85118e27ea..69f02fd46da 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -13,13 +13,14 @@ """Unit tests for BitArray.""" from itertools import product +from test import QiskitTestCase import ddt import numpy as np from qiskit.primitives.containers import BitArray +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.result import Counts -from test import QiskitTestCase # pylint: disable=wrong-import-order def u_8(arr): @@ -282,3 +283,415 @@ def test_reshape(self): self.assertEqual(ba.reshape(360 * 2, 16).shape, (720,)) self.assertEqual(ba.reshape(360 * 2, 16).num_shots, 16) self.assertEqual(ba.reshape(360 * 2, 16).num_bits, 15) + + def test_transpose(self): + """Test the transpose method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + # Since the input dtype is uint16, bit array requires at least two u8. + # Thus, 9 is the minimum number of qubits, i.e., 8 + 1. + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("default arg"): + ba2 = ba.transpose() + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("tuple 1"): + ba2 = ba.transpose((2, 1, 0)) + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("tuple 2"): + ba2 = ba.transpose((0, 1, 2)) + self.assertEqual(ba2.shape, (1, 2, 3)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("tuple 3"): + ba2 = ba.transpose((0, 2, 1)) + self.assertEqual(ba2.shape, (1, 3, 2)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, k, j))) + + with self.subTest("tuple, negative indices"): + ba2 = ba.transpose((0, -1, -2)) + self.assertEqual(ba2.shape, (1, 3, 2)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, k, j))) + + with self.subTest("ints"): + ba2 = ba.transpose(2, 1, 0) + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "axes don't match bit array"): + _ = ba.transpose((0, 1)) + with self.assertRaisesRegex(ValueError, "axes don't match bit array"): + _ = ba.transpose((0, 1, 2, 3)) + with self.assertRaisesRegex(ValueError, "axis [0-9]+ is out of bounds for bit array"): + _ = ba.transpose((0, 1, 4)) + with self.assertRaisesRegex(ValueError, "axis -[0-9]+ is out of bounds for bit array"): + _ = ba.transpose((0, 1, -4)) + with self.assertRaisesRegex(ValueError, "repeated axis in transpose"): + _ = ba.transpose((0, 1, 1)) + + def test_concatenate(self): + """Test the concatenate function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate = BitArray.concatenate + + with self.subTest("2 arrays, default"): + ba2 = concatenate([ba, ba]) + self.assertEqual(ba2.shape, (2, 2, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((1, j, k))) + + with self.subTest("2 arrays, axis"): + ba2 = concatenate([ba, ba], axis=1) + self.assertEqual(ba2.shape, (1, 4, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((0, j + 2, k))) + + with self.subTest("3 arrays"): + ba2 = concatenate([ba, ba, ba]) + self.assertEqual(ba2.shape, (3, 2, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((1, j, k))) + self.assertEqual(ba2.get_counts((1, j, k)), ba2.get_counts((2, j, k))) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to concatenate"): + _ = concatenate([]) + with self.assertRaisesRegex(ValueError, "axis -1 is out of bounds"): + _ = concatenate([ba, ba], -1) + with self.assertRaisesRegex(ValueError, "axis 100 is out of bounds"): + _ = concatenate([ba, ba], 100) + + ba2 = BitArray(data, 10) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same number of bits"): + _ = concatenate([ba, ba2]) + + data2 = np.frombuffer(np.arange(30, dtype=np.uint16).tobytes(), dtype=np.uint8) + data2 = data2.reshape(1, 2, 3, 5, 2)[..., ::-1] + ba2 = BitArray(data2, 9) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of shots" + ): + _ = concatenate([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of dimensions" + ): + _ = concatenate([ba, ba2]) + + def test_concatenate_shots(self): + """Test the concatenate_shots function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate_shots = BitArray.concatenate_shots + + with self.subTest("2 arrays"): + ba2 = concatenate_shots([ba, ba]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 9) + self.assertEqual(ba2.num_shots, 2 * ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + expected = {key: val * 2 for key, val in ba.get_counts((i, j, k)).items()} + counts2 = ba2.get_counts((i, j, k)) + self.assertEqual(counts2, expected) + + with self.subTest("3 arrays"): + ba2 = concatenate_shots([ba, ba, ba]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 9) + self.assertEqual(ba2.num_shots, 3 * ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + expected = {key: val * 3 for key, val in ba.get_counts((i, j, k)).items()} + counts2 = ba2.get_counts((i, j, k)) + self.assertEqual(counts2, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to stack"): + _ = concatenate_shots([]) + + ba2 = BitArray(data, 10) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same number of bits"): + _ = concatenate_shots([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same shape"): + _ = concatenate_shots([ba, ba2]) + + def test_concatenate_bits(self): + """Test the concatenate_bits function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate_bits = BitArray.concatenate_bits + + with self.subTest("2 arrays"): + ba_01 = ba.slice_bits([0, 1]) + ba2 = concatenate_bits([ba, ba_01]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 11) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + bs = ba.get_bitstrings((i, j, k)) + bs_01 = ba_01.get_bitstrings((i, j, k)) + expected = [s1 + s2 for s1, s2 in zip(bs_01, bs)] + bs2 = ba2.get_bitstrings((i, j, k)) + self.assertEqual(bs2, expected) + + with self.subTest("3 arrays"): + ba_01 = ba.slice_bits([0, 1]) + ba2 = concatenate_bits([ba, ba_01, ba_01]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 13) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + bs = ba.get_bitstrings((i, j, k)) + bs_01 = ba_01.get_bitstrings((i, j, k)) + expected = [s1 + s1 + s2 for s1, s2 in zip(bs_01, bs)] + bs2 = ba2.get_bitstrings((i, j, k)) + self.assertEqual(bs2, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to stack"): + _ = concatenate_bits([]) + + data2 = np.frombuffer(np.arange(30, dtype=np.uint16).tobytes(), dtype=np.uint8) + data2 = data2.reshape(1, 2, 3, 5, 2)[..., ::-1] + ba2 = BitArray(data2, 9) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of shots" + ): + _ = concatenate_bits([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same shape"): + _ = concatenate_bits([ba, ba2]) + + def test_getitem(self): + """Test the __getitem__ method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba[:] + self.assertEqual(ba2.shape, (1, 2, 3)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("no slice"): + ba2 = ba[0, 1, 2] + self.assertEqual(ba2.shape, ()) + self.assertEqual(ba.get_counts((0, 1, 2)), ba2.get_counts()) + + with self.subTest("slice"): + ba2 = ba[0, :, 2] + self.assertEqual(ba2.shape, (2,)) + for j in range(2): + self.assertEqual(ba.get_counts((0, j, 2)), ba2.get_counts(j)) + + def test_slice_bits(self): + """Test the slice_bits method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba.slice_bits(range(ba.num_bits)) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, ba.num_bits) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("1 bit, int"): + ba2 = ba.slice_bits(0) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_counts((i, j, k)), {"0": 5, "1": 5}) + + with self.subTest("1 bit, list"): + ba2 = ba.slice_bits([0]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_counts((i, j, k)), {"0": 5, "1": 5}) + + with self.subTest("2 bits"): + ba2 = ba.slice_bits([0, 1]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 2) + even = {"00": 3, "01": 3, "10": 2, "11": 2} + odd = {"10": 3, "11": 3, "00": 2, "01": 2} + for count, (i, j, k) in enumerate(product(range(1), range(2), range(3))): + expect = even if count % 2 == 0 else odd + self.assertEqual(ba2.get_counts((i, j, k)), expect) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + _ = ba.slice_bits(-1) + with self.assertRaisesRegex(ValueError, "index 9 is out of bounds"): + _ = ba.slice_bits(9) + + def test_slice_shots(self): + """Test the slice_shots method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba.slice_shots(range(ba.num_shots)) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("1 shot, int"): + ba2 = ba.slice_shots(0) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_bitstrings((i, j, k)), [ba.get_bitstrings((i, j, k))[0]]) + + with self.subTest("1 shot, list"): + ba2 = ba.slice_shots([0]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_bitstrings((i, j, k)), [ba.get_bitstrings((i, j, k))[0]]) + + with self.subTest("multiple shots"): + indices = [1, 2, 3, 5, 8] + ba2 = ba.slice_shots(indices) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, len(indices)) + for i, j, k in product(range(1), range(2), range(3)): + expected = [ + bs for ind, bs in enumerate(ba.get_bitstrings((i, j, k))) if ind in indices + ] + self.assertEqual(ba2.get_bitstrings((i, j, k)), expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + _ = ba.slice_shots(-1) + with self.assertRaisesRegex(ValueError, "index 10 is out of bounds"): + _ = ba.slice_shots(10) + + def test_expectation_values(self): + """Test the expectation_values method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + op = "I" * 8 + "Z" + op2 = "I" * 8 + "0" + op3 = "I" * 8 + "1" + pauli = Pauli(op) + sp_op = SparsePauliOp(op) + sp_op2 = SparsePauliOp.from_sparse_list([("Z", [6], 1)], num_qubits=9) + + with self.subTest("str"): + expval = ba.expectation_values(op) + # both 0 and 1 appear 5 times + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + expval = ba.expectation_values(op2) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.full((ba.shape), 0.5)) + + expval = ba.expectation_values(op3) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.full((ba.shape), 0.5)) + + ba2 = ba.slice_bits(6) + # 6th bit are all 0 + expval = ba2.expectation_values("Z") + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.ones(ba.shape)) + + ba3 = ba.slice_bits(5) + # 5th bit distributes as follows. + # (0, 0, 0) {'0': 10} + # (0, 0, 1) {'0': 10} + # (0, 0, 2) {'0': 10} + # (0, 1, 0) {'0': 2, '1': 8} + # (0, 1, 1) {'1': 10} + # (0, 1, 2) {'1': 10} + expval = ba3.expectation_values("0") + expected = np.array([[[1, 1, 1], [0.2, 0, 0]]]) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, expected) + + with self.subTest("Pauli"): + expval = ba.expectation_values(pauli) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + with self.subTest("SparsePauliOp"): + expval = ba.expectation_values(sp_op) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + expval = ba.expectation_values(sp_op2) + # 6th bit are all 0 + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.ones((ba.shape))) + + with self.subTest("ObservableArray"): + obs = ["Z", "0", "1"] + ba2 = ba.slice_bits(5) + expval = ba2.expectation_values(obs) + expected = np.array([[[1, 1, 0], [-0.6, 0, 1]]]) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, expected) + + ba4 = BitArray.from_counts([{0: 1}, {1: 1}]).reshape(2, 1) + expval = ba4.expectation_values(obs) + expected = np.array([[1, 1, 0], [-1, 0, 1]]) + self.assertEqual(expval.shape, (2, 3)) + np.testing.assert_allclose(expval, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "shape mismatch"): + _ = ba.expectation_values([op, op2]) + with self.assertRaisesRegex(ValueError, "One or more operators not same length"): + _ = ba.expectation_values("Z") + with self.assertRaisesRegex(ValueError, "is not diagonal"): + _ = ba.expectation_values("X" * ba.num_bits) From f34fb21219077311eb0e3f35e732c50ba4eb4101 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 May 2024 13:25:15 -0400 Subject: [PATCH 030/159] Add star to linear pre-routing pass (#11387) * Add Star to linear pre-routing pass This commit adds a new transpiler pass StarPreRouting that adds a dedicated transformation that finds a star graph connectivity subcircuit and replaces it with a linear routing equivalent. This makes the circuit easier for a routing pass to work with. * Add missing doc imports * Fix handling of 1q gates and swap tracking This commit fixes two related issues. The first is that it fixed the handling of 1q gates in the middle of a star sequence. The sequence collection was correctly skipping the 1q gates, but when we reassembled the circuit and applied the linear routing it would incorrectly build the routing without the 1q gates. This would result in the 1q gates being pushed after the star. At the same time the other related issue was that the permutations caused by the swap insertions were not taken into account for subsequent gates. This would result in gates being placed on the wrong bits and incorrect results being returned. * fixed/tested star-prerouting * Apply suggestions from code review * changed moved self.dag to a function parameter * Removing the usage of FinalizeLayout in star prerouting tests * format --------- Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> --- qiskit/transpiler/passes/__init__.py | 2 + qiskit/transpiler/passes/routing/__init__.py | 1 + .../passes/routing/star_prerouting.py | 415 +++++++++++++++ .../star-prerouting-0998b59880c20cef.yaml | 32 ++ .../python/transpiler/test_star_prerouting.py | 484 ++++++++++++++++++ 5 files changed, 934 insertions(+) create mode 100644 qiskit/transpiler/passes/routing/star_prerouting.py create mode 100644 releasenotes/notes/star-prerouting-0998b59880c20cef.yaml create mode 100644 test/python/transpiler/test_star_prerouting.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index c1e1705f1cc..54599e00b9a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -46,6 +46,7 @@ StochasticSwap SabreSwap Commuting2qGateRouter + StarPreRouting Basis Change ============ @@ -205,6 +206,7 @@ from .routing import StochasticSwap from .routing import SabreSwap from .routing import Commuting2qGateRouter +from .routing import StarPreRouting # basis change from .basis import Decompose diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index 2316705b4a1..a1ac25fb414 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -19,3 +19,4 @@ from .sabre_swap import SabreSwap from .commuting_2q_gate_routing.commuting_2q_gate_router import Commuting2qGateRouter from .commuting_2q_gate_routing.swap_strategy import SwapStrategy +from .star_prerouting import StarPreRouting diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py new file mode 100644 index 00000000000..8e278471295 --- /dev/null +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -0,0 +1,415 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Search for star connectivity patterns and replace them with.""" +from typing import Iterable, Union, Optional, List, Tuple +from math import floor, log10 + +from qiskit.circuit import Barrier +from qiskit.dagcircuit import DAGOpNode, DAGDepNode, DAGDependency, DAGCircuit +from qiskit.transpiler import Layout +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.library import SwapGate + + +class StarBlock: + """Defines blocks representing star-shaped pieces of a circuit.""" + + def __init__(self, nodes=None, center=None, num2q=0): + self.center = center + self.num2q = num2q + self.nodes = [] if nodes is None else nodes + + def get_nodes(self): + """Returns the list of nodes used in the block.""" + return self.nodes + + def append_node(self, node): + """ + If node can be added to block while keeping the block star-shaped, and + return True. Otherwise, does not add node to block and returns False. + """ + + added = False + + if len(node.qargs) == 1: + self.nodes.append(node) + added = True + elif self.center is None: + self.center = set(node.qargs) + self.nodes.append(node) + self.num2q += 1 + added = True + elif isinstance(self.center, set): + if node.qargs[0] in self.center: + self.center = node.qargs[0] + self.nodes.append(node) + self.num2q += 1 + added = True + elif node.qargs[1] in self.center: + self.center = node.qargs[1] + self.nodes.append(node) + self.num2q += 1 + added = True + else: + if self.center in node.qargs: + self.nodes.append(node) + self.num2q += 1 + added = True + + return added + + def size(self): + """ + Returns the number of two-qubit quantum gates in this block. + """ + return self.num2q + + +class StarPreRouting(TransformationPass): + """Run star to linear pre-routing + + This pass is a logical optimization pass that rewrites any + solely 2q gate star connectivity subcircuit as a linear connectivity + equivalent with swaps. + + For example: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import StarPreRouting + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + StarPreRouting()(qc).draw("mpl") + + This pass was inspired by a similar pass described in Section IV of: + C. Campbell et al., "Superstaq: Deep Optimization of Quantum Programs," + 2023 IEEE International Conference on Quantum Computing and Engineering (QCE), + Bellevue, WA, USA, 2023, pp. 1020-1032, doi: 10.1109/QCE57702.2023.00116. + """ + + def __init__(self): + """StarPreRouting""" + + self._pending_nodes: Optional[list[Union[DAGOpNode, DAGDepNode]]] = None + self._in_degree: Optional[dict[Union[DAGOpNode, DAGDepNode], int]] = None + super().__init__() + + def _setup_in_degrees(self, dag): + """For an efficient implementation, for every node we keep the number of its + unprocessed immediate predecessors (called ``_in_degree``). This ``_in_degree`` + is set up at the start and updated throughout the algorithm. + A node is leaf (or input) node iff its ``_in_degree`` is 0. + When a node is (marked as) collected, the ``_in_degree`` of each of its immediate + successor is updated by subtracting 1. + Additionally, ``_pending_nodes`` explicitly keeps the list of nodes whose + ``_in_degree`` is 0. + """ + self._pending_nodes = [] + self._in_degree = {} + for node in self._op_nodes(dag): + deg = len(self._direct_preds(dag, node)) + self._in_degree[node] = deg + if deg == 0: + self._pending_nodes.append(node) + + def _op_nodes(self, dag) -> Iterable[Union[DAGOpNode, DAGDepNode]]: + """Returns DAG nodes.""" + if not isinstance(dag, DAGDependency): + return dag.op_nodes() + else: + return dag.get_nodes() + + def _direct_preds(self, dag, node): + """Returns direct predecessors of a node. This function takes into account the + direction of collecting blocks, that is node's predecessors when collecting + backwards are the direct successors of a node in the DAG. + """ + if not isinstance(dag, DAGDependency): + return [pred for pred in dag.predecessors(node) if isinstance(pred, DAGOpNode)] + else: + return [dag.get_node(pred_id) for pred_id in dag.direct_predecessors(node.node_id)] + + def _direct_succs(self, dag, node): + """Returns direct successors of a node. This function takes into account the + direction of collecting blocks, that is node's successors when collecting + backwards are the direct predecessors of a node in the DAG. + """ + if not isinstance(dag, DAGDependency): + return [succ for succ in dag.successors(node) if isinstance(succ, DAGOpNode)] + else: + return [dag.get_node(succ_id) for succ_id in dag.direct_successors(node.node_id)] + + def _have_uncollected_nodes(self): + """Returns whether there are uncollected (pending) nodes""" + return len(self._pending_nodes) > 0 + + def collect_matching_block(self, dag, filter_fn): + """Iteratively collects the largest block of input nodes (that is, nodes with + ``_in_degree`` equal to 0) that match a given filtering function. + Examples of this include collecting blocks of swap gates, + blocks of linear gates (CXs and SWAPs), blocks of Clifford gates, blocks of single-qubit gates, + blocks of two-qubit gates, etc. Here 'iteratively' means that once a node is collected, + the ``_in_degree`` of each of its immediate successor is decreased by 1, allowing more nodes + to become input and to be eligible for collecting into the current block. + Returns the block of collected nodes. + """ + unprocessed_pending_nodes = self._pending_nodes + self._pending_nodes = [] + + current_block = StarBlock() + + # Iteratively process unprocessed_pending_nodes: + # - any node that does not match filter_fn is added to pending_nodes + # - any node that match filter_fn is added to the current_block, + # and some of its successors may be moved to unprocessed_pending_nodes. + while unprocessed_pending_nodes: + new_pending_nodes = [] + for node in unprocessed_pending_nodes: + added = filter_fn(node) and current_block.append_node(node) + if added: + # update the _in_degree of node's successors + for suc in self._direct_succs(dag, node): + self._in_degree[suc] -= 1 + if self._in_degree[suc] == 0: + new_pending_nodes.append(suc) + else: + self._pending_nodes.append(node) + unprocessed_pending_nodes = new_pending_nodes + + return current_block + + def collect_all_matching_blocks( + self, + dag, + min_block_size=2, + ): + """Collects all blocks that match a given filtering function filter_fn. + This iteratively finds the largest block that does not match filter_fn, + then the largest block that matches filter_fn, and so on, until no more uncollected + nodes remain. Intuitively, finding larger blocks of non-matching nodes helps to + find larger blocks of matching nodes later on. The option ``min_block_size`` + specifies the minimum number of gates in the block for the block to be collected. + + By default, blocks are collected in the direction from the inputs towards the outputs + of the circuit. The option ``collect_from_back`` allows to change this direction, + that is collect blocks from the outputs towards the inputs of the circuit. + + Returns the list of matching blocks only. + """ + + def filter_fn(node): + """Specifies which nodes can be collected into star blocks.""" + return ( + len(node.qargs) <= 2 + and len(node.cargs) == 0 + and getattr(node.op, "condition", None) is None + and not isinstance(node.op, Barrier) + ) + + def not_filter_fn(node): + """Returns the opposite of filter_fn.""" + return not filter_fn(node) + + # Note: the collection direction must be specified before setting in-degrees + self._setup_in_degrees(dag) + + # Iteratively collect non-matching and matching blocks. + matching_blocks: list[StarBlock] = [] + processing_order = [] + while self._have_uncollected_nodes(): + self.collect_matching_block(dag, filter_fn=not_filter_fn) + matching_block = self.collect_matching_block(dag, filter_fn=filter_fn) + if matching_block.size() >= min_block_size: + matching_blocks.append(matching_block) + processing_order.append(matching_block) + + processing_order = [n for p in processing_order for n in p.nodes] + + return matching_blocks, processing_order + + def run(self, dag): + # Extract StarBlocks from DAGCircuit / DAGDependency / DAGDependencyV2 + star_blocks, processing_order = self.determine_star_blocks_processing(dag, min_block_size=2) + + if not star_blocks: + return dag + + if all(b.size() < 3 for b in star_blocks): + # we only process blocks with less than 3 two-qubit gates in this pre-routing pass + # if they occur in a collection of larger stars, otherwise we consider them to be 'lines' + return dag + + # Create a new DAGCircuit / DAGDependency / DAGDependencyV2, replacing each + # star block by a linear sequence of gates + new_dag, qubit_mapping = self.star_preroute(dag, star_blocks, processing_order) + + # Fix output permuation -- copied from ElidePermutations + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + + new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) + if current_layout := self.property_set["virtual_permutation_layout"] is not None: + self.property_set["virtual_permutation_layout"] = current_layout.compose(new_layout) + else: + self.property_set["virtual_permutation_layout"] = new_layout + + return new_dag + + def determine_star_blocks_processing( + self, dag: Union[DAGCircuit, DAGDependency], min_block_size: int + ) -> Tuple[List[StarBlock], Union[List[DAGOpNode], List[DAGDepNode]]]: + """Returns star blocks in dag and the processing order of nodes within these star blocks + Args: + dag (DAGCircuit or DAGDependency): a dag on which star blocks should be determined. + min_block_size (int): minimum number of two-qubit gates in a star block. + + Returns: + List[StarBlock]: a list of star blocks in the given dag + Union[List[DAGOpNode], List[DAGDepNode]]: a list of operations specifying processing order + """ + blocks, processing_order = self.collect_all_matching_blocks( + dag, min_block_size=min_block_size + ) + return blocks, processing_order + + def star_preroute(self, dag, blocks, processing_order): + """Returns star blocks in dag and the processing order of nodes within these star blocks + Args: + dag (DAGCircuit or DAGDependency): a dag on which star prerouting should be performed. + blocks (List[StarBlock]): a list of star blocks in the given dag. + processing_order (Union[List[DAGOpNode], List[DAGDepNode]]): a list of operations specifying + processing order + + Returns: + 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")) + ] + if len(last_2q_gate) > 0: + last_2q_gate = last_2q_gate[0] + else: + last_2q_gate = None + + int_digits = floor(log10(len(processing_order))) + 1 + processing_order_s = set(processing_order) + + def tie_breaker_key(node): + if node in processing_order_s: + return "a" + str(processing_order.index(node)).zfill(int(int_digits)) + else: + return 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 diff --git a/releasenotes/notes/star-prerouting-0998b59880c20cef.yaml b/releasenotes/notes/star-prerouting-0998b59880c20cef.yaml new file mode 100644 index 00000000000..0bf60329a23 --- /dev/null +++ b/releasenotes/notes/star-prerouting-0998b59880c20cef.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Added a new transpiler pass :class:`.StarPreRouting` which is designed to identify star connectivity subcircuits + and then replace them with an optimal linear routing. This is useful for certain circuits that are composed of + this circuit connectivity such as Berstein Vazirani and QFT. For example: + + .. plot: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + qc.draw("mpl") + + .. plot: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import StarPreRouting + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + StarPreRouting()(qc).draw("mpl") diff --git a/test/python/transpiler/test_star_prerouting.py b/test/python/transpiler/test_star_prerouting.py new file mode 100644 index 00000000000..ddc8096eefd --- /dev/null +++ b/test/python/transpiler/test_star_prerouting.py @@ -0,0 +1,484 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-function-docstring + +"""Test the StarPreRouting pass""" + +import unittest +from test import QiskitTestCase +import ddt + +from qiskit.circuit.library import QFT +from qiskit.circuit.quantumcircuit import QuantumCircuit +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 +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.utils.optionals import HAS_AER + + +@ddt.ddt +class TestStarPreRouting(QiskitTestCase): + """Tests the StarPreRouting pass""" + + def test_simple_ghz_dagcircuit(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, range(1, 5)) + dag = circuit_to_dag(qc) + new_dag = StarPreRouting().run(dag) + new_qc = dag_to_circuit(new_dag) + + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.cx(0, 2) + expected.swap(0, 2) + expected.cx(2, 3) + expected.swap(2, 3) + expected.cx(3, 4) + # expected.swap(3,4) + + self.assertTrue(Operator(expected).equiv(Operator(new_qc))) + + def test_simple_ghz_dagdependency(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, range(1, 5)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + def test_double_ghz_dagcircuit(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + new_qc = pm.run(qc) + + self.assertTrue(Operator.from_circuit(new_qc).equiv(Operator(qc))) + + def test_double_ghz_dagdependency(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + new_qc = pm.run(qc) + + self.assertTrue(Operator(qc).equiv(Operator.from_circuit(new_qc))) + + def test_mixed_double_ghz_dagdependency(self): + """Shows off the power of using commutation analysis.""" + qc = QuantumCircuit(4) + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + # qc.measure_all() + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + def test_double_ghz(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + result = pm.run(qc) + + self.assertEqual(Operator.from_circuit(result), Operator(qc)) + + def test_linear_ghz_no_change(self): + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 5) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertEqual(Operator.from_circuit(result), Operator(qc)) + + def test_no_star(self): + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(3, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(1, 4) + qc.cx(2, 1) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(qc)) + + def test_10q_bv(self): + num_qubits = 10 + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.barrier() + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + result = StarPreRouting()(qc) + + expected = QuantumCircuit(num_qubits, num_qubits - 1) + expected.h(0) + expected.h(1) + expected.h(2) + expected.h(3) + expected.h(4) + expected.h(5) + expected.h(6) + expected.h(7) + expected.h(8) + expected.x(9) + expected.h(9) + expected.cx(0, 9) + expected.cx(1, 9) + expected.swap(1, 9) + expected.cx(2, 1) + expected.swap(2, 1) + expected.cx(3, 2) + expected.swap(3, 2) + expected.cx(4, 3) + expected.swap(4, 3) + expected.cx(5, 4) + expected.swap(5, 4) + expected.cx(6, 5) + expected.swap(6, 5) + expected.cx(7, 6) + expected.swap(7, 6) + expected.cx(8, 7) + expected.barrier() + expected.h(0) + expected.h(1) + expected.h(2) + expected.h(3) + expected.h(4) + expected.h(5) + expected.h(6) + expected.h(8) + expected.h(9) + expected.measure(0, 0) + expected.measure(9, 1) + expected.measure(1, 2) + expected.measure(2, 3) + expected.measure(3, 4) + expected.measure(4, 5) + expected.measure(5, 6) + expected.measure(6, 7) + expected.measure(8, 8) + self.assertEqual(result, expected) + + # Skip level 3 because of unitary synth introducing non-clifford gates + @unittest.skipUnless(HAS_AER, "Aer required for clifford simulation") + @ddt.data(0, 1) + def test_100q_grid_full_path(self, opt_level): + from qiskit_aer import AerSimulator + + num_qubits = 100 + coupling_map = CouplingMap.from_grid(10, 10) + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.barrier() + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + pm = generate_preset_pass_manager( + opt_level, basis_gates=["h", "cx", "x"], coupling_map=coupling_map + ) + pm.init += StarPreRouting() + result = pm.run(qc) + counts_before = AerSimulator().run(qc).result().get_counts() + counts_after = AerSimulator().run(result).result().get_counts() + self.assertEqual(counts_before, counts_after) + + def test_10q_bv_no_barrier(self): + num_qubits = 6 + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.h(qc.qubits[:-1]) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + # Skip level 3 because of unitary synth introducing non-clifford gates + @unittest.skipUnless(HAS_AER, "Aer required for clifford simulation") + @ddt.data(0, 1) + def test_100q_grid_full_path_no_barrier(self, opt_level): + from qiskit_aer import AerSimulator + + num_qubits = 100 + coupling_map = CouplingMap.from_grid(10, 10) + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + pm = generate_preset_pass_manager( + opt_level, basis_gates=["h", "cx", "x"], coupling_map=coupling_map + ) + pm.init += StarPreRouting() + result = pm.run(qc) + counts_before = AerSimulator().run(qc).result().get_counts() + counts_after = AerSimulator().run(result).result().get_counts() + self.assertEqual(counts_before, counts_after) + + def test_hadamard_ordering(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.h(0) + qc.cx(0, 2) + qc.h(0) + qc.cx(0, 3) + qc.h(0) + qc.cx(0, 4) + result = StarPreRouting()(qc) + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.h(0) + expected.cx(0, 2) + expected.swap(0, 2) + expected.h(2) + expected.cx(2, 3) + expected.swap(2, 3) + expected.h(3) + expected.cx(3, 4) + # expected.swap(3, 4) + self.assertEqual(expected, result) + + def test_count_1_stars_starting_center(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + spr = StarPreRouting() + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 1) + self.assertEqual(len(star_blocks[0].nodes), 5) + + def test_count_1_stars_starting_branch(self): + qc = QuantumCircuit(6) + qc.cx(1, 0) + qc.cx(2, 0) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + spr = StarPreRouting() + _ = spr(qc) + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 1) + self.assertEqual(len(star_blocks[0].nodes), 5) + + def test_count_2_stars(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + + qc.cx(1, 2) + qc.cx(1, 3) + qc.cx(1, 4) + qc.cx(1, 5) + spr = StarPreRouting() + _ = spr(qc) + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 2) + self.assertEqual(len(star_blocks[0].nodes), 5) + self.assertEqual(len(star_blocks[1].nodes), 4) + + def test_count_3_stars(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + + qc.cx(1, 2) + qc.cx(1, 3) + qc.cx(1, 4) + qc.cx(1, 5) + + qc.cx(2, 3) + qc.cx(2, 4) + qc.cx(2, 5) + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + + self.assertEqual(len(star_blocks), 3) + self.assertEqual(len(star_blocks[0].nodes), 5) + self.assertEqual(len(star_blocks[1].nodes), 4) + self.assertEqual(len(star_blocks[2].nodes), 3) + + def test_count_70_qft_stars(self): + qft_module = QFT(10, do_swaps=False).decompose() + qftqc = QuantumCircuit(100) + for i in range(10): + qftqc.compose(qft_module, qubits=range(i * 10, (i + 1) * 10), inplace=True) + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing( + circuit_to_dag(qftqc), min_block_size=2 + ) + + self.assertEqual(len(star_blocks), 80) + star_len_list = [len([n for n in b.nodes if len(n.qargs) > 1]) for b in star_blocks] + expected_star_size = {2, 3, 4, 5, 6, 7, 8, 9} + self.assertEqual(set(star_len_list), expected_star_size) + for i in expected_star_size: + self.assertEqual(star_len_list.count(i), 10) + + def test_count_50_qft_stars(self): + qft_module = QFT(10, do_swaps=False).decompose() + qftqc = QuantumCircuit(10) + for _ in range(10): + qftqc.compose(qft_module, qubits=range(10), inplace=True) + spr = StarPreRouting() + _ = spr(qftqc) + + star_blocks, _ = spr.determine_star_blocks_processing( + circuit_to_dag(qftqc), min_block_size=2 + ) + self.assertEqual(len(star_blocks), 50) + star_len_list = [len([n for n in b.nodes if len(n.qargs) > 1]) for b in star_blocks] + expected_star_size = {9} + self.assertEqual(set(star_len_list), expected_star_size) + + def test_two_star_routing(self): + qc = QuantumCircuit(4) + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(2, 3) + qc.cx(2, 1) + + spr = StarPreRouting() + res = spr(qc) + + self.assertTrue(Operator.from_circuit(res).equiv(qc)) + + def test_detect_two_opposite_stars_barrier(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.barrier() + qc.cx(5, 1) + qc.cx(5, 2) + qc.cx(5, 3) + qc.cx(5, 4) + + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 2) + self.assertEqual(len(star_blocks[0].nodes), 4) + self.assertEqual(len(star_blocks[1].nodes), 4) + + def test_routing_after_star_prerouting(self): + nq = 6 + qc = QFT(nq, do_swaps=False, insert_barriers=True).decompose() + cm = CouplingMap.from_line(nq) + + pm_preroute = PassManager() + pm_preroute.append(StarPreRouting()) + pm_preroute.append(VF2Layout(coupling_map=cm, seed=17)) + pm_preroute.append(ApplyLayout()) + pm_preroute.append(SabreSwap(coupling_map=cm, seed=17)) + + pm_sabre = PassManager() + pm_sabre.append(SabreLayout(coupling_map=cm, seed=17)) + + res_sabre = pm_sabre.run(qc) + res_star = pm_sabre.run(qc) + + 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)) From bb1ef16abc1b9e8ff2d87c4c26ece0ff8e481b57 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 2 May 2024 20:34:15 +0300 Subject: [PATCH 031/159] Conjugate reduction in optimize annotated pass (#11811) * initial commit for the conjugate reduction optimization * do not go into definitions when unnecessary * release notes * typo * minor * rewriting as list comprehension * improving release notes * removing print statement * changing op_predecessors and op_successor methods to return iterators rather than lists * and removing explcit iter * constructing the optimized circuit using compose rather than append * improving variable names * adding test * adding tests exploring which gates get collected * more renaming --------- Co-authored-by: Matthew Treinish --- qiskit/dagcircuit/dagcircuit.py | 8 + .../passes/optimization/optimize_annotated.py | 258 +++++++++++++++++- ...-conjugate-reduction-656438d3642f27dc.yaml | 24 ++ .../transpiler/test_optimize_annotated.py | 226 ++++++++++++++- 4 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 838e9cfe0f8..831851bee36 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1879,6 +1879,14 @@ def predecessors(self, node): """Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes.""" return iter(self._multi_graph.predecessors(node._node_id)) + def op_successors(self, node): + """Returns iterator of "op" successors of a node in the dag.""" + return (succ for succ in self.successors(node) if isinstance(succ, DAGOpNode)) + + def op_predecessors(self, node): + """Returns the iterator of "op" predecessors of a node in the dag.""" + return (pred for pred in self.predecessors(node) if isinstance(pred, DAGOpNode)) + def is_successor(self, node, node_succ): """Checks if a second node is in the successors of node.""" return self._multi_graph.has_edge(node._node_id, node_succ._node_id) diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index 0b9b786a07f..fe6fe7f49e7 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -12,12 +12,19 @@ """Optimize annotated operations on a circuit.""" -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.annotated_operation import AnnotatedOperation, _canonicalize_modifiers -from qiskit.circuit import EquivalenceLibrary, ControlledGate, Operation, ControlFlowOp +from qiskit.circuit import ( + QuantumCircuit, + Instruction, + EquivalenceLibrary, + ControlledGate, + Operation, + ControlFlowOp, +) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target @@ -43,6 +50,11 @@ class OptimizeAnnotated(TransformationPass): ``g2 = AnnotatedOperation(g1, ControlModifier(2))``, then ``g2`` can be replaced with ``AnnotatedOperation(SwapGate(), [InverseModifier(), ControlModifier(2)])``. + * Applies conjugate reduction to annotated operations. As an example, + ``control - [P -- Q -- P^{-1}]`` can be rewritten as ``P -- control - [Q] -- P^{-1}``, + that is, only the middle part needs to be controlled. This also works for inverse + and power modifiers. + """ def __init__( @@ -51,6 +63,7 @@ def __init__( equivalence_library: Optional[EquivalenceLibrary] = None, basis_gates: Optional[List[str]] = None, recurse: bool = True, + do_conjugate_reduction: bool = True, ): """ OptimizeAnnotated initializer. @@ -67,12 +80,14 @@ def __init__( not applied when neither is specified since such objects do not need to be synthesized). Setting this value to ``False`` precludes the recursion in every case. + do_conjugate_reduction: controls whether conjugate reduction should be performed. """ super().__init__() self._target = target self._equiv_lib = equivalence_library self._basis_gates = basis_gates + self._do_conjugate_reduction = do_conjugate_reduction self._top_level_only = not recurse or (self._basis_gates is None and self._target is None) @@ -122,7 +137,11 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: # as they may remove annotated gates. dag, opt2 = self._recurse(dag) - return dag, opt1 or opt2 + opt3 = False + if not self._top_level_only and self._do_conjugate_reduction: + dag, opt3 = self._conjugate_reduction(dag) + + return dag, opt1 or opt2 or opt3 def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: """ @@ -148,17 +167,219 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: did_something = True return dag, did_something - def _recursively_process_definitions(self, op: Operation) -> bool: + def _conjugate_decomposition( + self, dag: DAGCircuit + ) -> Union[Tuple[DAGCircuit, DAGCircuit, DAGCircuit], None]: """ - Recursively applies optimizations to op's definition (or to op.base_op's - definition if op is an annotated operation). - Returns True if did something. + Decomposes a circuit ``A`` into 3 sub-circuits ``P``, ``Q``, ``R`` such that + ``A = P -- Q -- R`` and ``R = P^{-1}``. + + This is accomplished by iteratively finding inverse nodes at the front and at the back of the + circuit. """ - # If op is an annotated operation, we descend into its base_op - if isinstance(op, AnnotatedOperation): - return self._recursively_process_definitions(op.base_op) + front_block = [] # nodes collected from the front of the circuit (aka P) + back_block = [] # nodes collected from the back of the circuit (aka R) + + # Stores in- and out- degree for each node. These degrees are computed at the start of this + # function and are updated when nodes are collected into front_block or into back_block. + in_degree = {} + out_degree = {} + + # We use dicts to track for each qubit a DAG node at the front of the circuit that involves + # this qubit and a DAG node at the end of the circuit that involves this qubit (when exist). + # Note that for the DAGCircuit structure for each qubit there can be at most one such front + # and such back node. + # This allows for an efficient way to find an inverse pair of gates (one from the front and + # one from the back of the circuit). + # A qubit that was never examined does not appear in these dicts, and a qubit that was examined + # but currently is not involved at the front (resp. at the back) of the circuit has the value of + # None. + front_node_for_qubit = {} + back_node_for_qubit = {} + + # Keep the set of nodes that have been moved either to front_block or to back_block + processed_nodes = set() + + # Keep the set of qubits that are involved in nodes at the front or at the back of the circuit. + # When looking for inverse pairs of gates we will only iterate over these qubits. + active_qubits = set() + + # Keep pairs of nodes for which the inverse check was performed and the nodes + # were found to be not inverse to each other (memoization). + checked_node_pairs = set() + + # compute in- and out- degree for every node + # also update information for nodes at the start and at the end of the circuit + for node in dag.op_nodes(): + preds = list(dag.op_predecessors(node)) + in_degree[node] = len(preds) + if len(preds) == 0: + for q in node.qargs: + front_node_for_qubit[q] = node + active_qubits.add(q) + succs = list(dag.op_successors(node)) + out_degree[node] = len(succs) + if len(succs) == 0: + for q in node.qargs: + back_node_for_qubit[q] = node + active_qubits.add(q) + + # iterate while there is a possibility to find more inverse pairs + while len(active_qubits) > 0: + to_check = active_qubits.copy() + active_qubits.clear() + + # For each qubit q, check whether the gate at the front of the circuit that involves q + # and the gate at the end of the circuit that involves q are inverse + for q in to_check: + + if (front_node := front_node_for_qubit.get(q, None)) is None: + continue + if (back_node := back_node_for_qubit.get(q, None)) is None: + continue + + # front_node or back_node could be already collected when considering other qubits + if front_node in processed_nodes or back_node in processed_nodes: + continue + + # it is possible that the same node is both at the front and at the back, + # it should not be collected + if front_node == back_node: + continue + + # have been checked before + if (front_node, back_node) in checked_node_pairs: + continue + + # fast check based on the arguments + if front_node.qargs != back_node.qargs or front_node.cargs != back_node.cargs: + continue + + # in the future we want to include a more precise check whether a pair + # of nodes are inverse + if front_node.op == back_node.op.inverse(): + # update front_node_for_qubit and back_node_for_qubit + for q in front_node.qargs: + front_node_for_qubit[q] = None + for q in back_node.qargs: + back_node_for_qubit[q] = None + + # see which other nodes become at the front and update information + for node in dag.op_successors(front_node): + if node not in processed_nodes: + in_degree[node] -= 1 + if in_degree[node] == 0: + for q in node.qargs: + front_node_for_qubit[q] = node + active_qubits.add(q) + + # see which other nodes become at the back and update information + for node in dag.op_predecessors(back_node): + if node not in processed_nodes: + out_degree[node] -= 1 + if out_degree[node] == 0: + for q in node.qargs: + back_node_for_qubit[q] = node + active_qubits.add(q) + + # collect and mark as processed + front_block.append(front_node) + back_block.append(back_node) + processed_nodes.add(front_node) + processed_nodes.add(back_node) + + else: + checked_node_pairs.add((front_node, back_node)) + + # if nothing is found, return None + if len(front_block) == 0: + return None + + # create the output DAGs + front_circuit = dag.copy_empty_like() + middle_circuit = dag.copy_empty_like() + back_circuit = dag.copy_empty_like() + front_circuit.global_phase = 0 + back_circuit.global_phase = 0 + + for node in front_block: + front_circuit.apply_operation_back(node.op, node.qargs, node.cargs) + + for node in back_block: + back_circuit.apply_operation_front(node.op, node.qargs, node.cargs) + + for node in dag.op_nodes(): + if node not in processed_nodes: + middle_circuit.apply_operation_back(node.op, node.qargs, node.cargs) + + return front_circuit, middle_circuit, back_circuit + + def _conjugate_reduce_op( + self, op: AnnotatedOperation, base_decomposition: Tuple[DAGCircuit, DAGCircuit, DAGCircuit] + ) -> Operation: + """ + We are given an annotated-operation ``op = M [ B ]`` (where ``B`` is the base operation and + ``M`` is the list of modifiers) and the "conjugate decomposition" of the definition of ``B``, + i.e. ``B = P * Q * R``, with ``R = P^{-1}`` (with ``P``, ``Q`` and ``R`` represented as + ``DAGCircuit`` objects). + + Let ``IQ`` denote a new custom instruction with definitions ``Q``. + + We return the operation ``op_new`` which a new custom instruction with definition + ``P * A * R``, where ``A`` is a new annotated-operation with modifiers ``M`` and + base gate ``IQ``. + """ + p_dag, q_dag, r_dag = base_decomposition + + q_instr = Instruction( + name="iq", num_qubits=op.base_op.num_qubits, num_clbits=op.base_op.num_clbits, params=[] + ) + q_instr.definition = dag_to_circuit(q_dag) + + op_new = Instruction( + "optimized", num_qubits=op.num_qubits, num_clbits=op.num_clbits, params=[] + ) + num_control_qubits = op.num_qubits - op.base_op.num_qubits + + circ = QuantumCircuit(op.num_qubits, op.num_clbits) + qubits = circ.qubits + circ.compose( + dag_to_circuit(p_dag), qubits[num_control_qubits : op.num_qubits], inplace=True + ) + circ.append( + AnnotatedOperation(base_op=q_instr, modifiers=op.modifiers), range(op.num_qubits) + ) + circ.compose( + dag_to_circuit(r_dag), qubits[num_control_qubits : op.num_qubits], inplace=True + ) + op_new.definition = circ + return op_new + + def _conjugate_reduction(self, dag) -> Tuple[DAGCircuit, bool]: + """ + Looks for annotated operations whose base operation has a nontrivial conjugate decomposition. + In such cases, the modifiers of the annotated operation can be moved to the "middle" part of + the decomposition. + Returns the modified DAG and whether it did something. + """ + did_something = False + for node in dag.op_nodes(op=AnnotatedOperation): + base_op = node.op.base_op + if not self._skip_definition(base_op): + base_dag = circuit_to_dag(base_op.definition, copy_operations=False) + base_decomposition = self._conjugate_decomposition(base_dag) + if base_decomposition is not None: + new_op = self._conjugate_reduce_op(node.op, base_decomposition) + dag.substitute_node(node, new_op) + did_something = True + return dag, did_something + + def _skip_definition(self, op: Operation) -> bool: + """ + Returns True if we should not recurse into a gate's definition. + """ # Similar to HighLevelSynthesis transpiler pass, we do not recurse into a gate's # `definition` for a gate that is supported by the target or in equivalence library. @@ -170,7 +391,22 @@ def _recursively_process_definitions(self, op: Operation) -> bool: else op.name in self._device_insts ) if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)): - return False + return True + return False + + def _recursively_process_definitions(self, op: Operation) -> bool: + """ + Recursively applies optimizations to op's definition (or to op.base_op's + definition if op is an annotated operation). + Returns True if did something. + """ + + # If op is an annotated operation, we descend into its base_op + if isinstance(op, AnnotatedOperation): + return self._recursively_process_definitions(op.base_op) + + if self._skip_definition(op): + return False try: # extract definition diff --git a/releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml b/releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml new file mode 100644 index 00000000000..231f0e7c8f3 --- /dev/null +++ b/releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml @@ -0,0 +1,24 @@ +features: + - | + Added a new reduction to the :class:`.OptimizeAnnotated` transpiler pass. + This reduction looks for annotated operations (objects of type :class:`.AnnotatedOperation` + that consist of a base operation ``B`` and a list ``M`` of control, inverse and power + modifiers) with the following properties: + + * the base operation ``B`` needs to be synthesized (i.e. it's not already supported + by the target or belongs to the equivalence library) + + * the definition circuit for ``B`` can be expressed as ``P -- Q -- R`` with :math:`R = P^{-1}` + + In this case the modifiers can be moved to the ``Q``-part only. As a specific example, + controlled QFT-based adders have the form ``control - [QFT -- U -- IQFT]``, which can be + simplified to ``QFT -- control-[U] -- IQFT``. By removing the controls over ``QFT`` and + ``IQFT`` parts of the circuit, one obtains significantly fewer gates in the transpiled + circuit. + - | + Added two new methods to the :class:`~qiskit.dagcircuit.DAGCircuit` class: + :meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to + :class:`.DAGOpNode` successors of a node, and + :meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to + :class:`.DAGOpNode` predecessors of a node. + diff --git a/test/python/transpiler/test_optimize_annotated.py b/test/python/transpiler/test_optimize_annotated.py index 5e573b551dd..0b15b79e2cf 100644 --- a/test/python/transpiler/test_optimize_annotated.py +++ b/test/python/transpiler/test_optimize_annotated.py @@ -22,6 +22,7 @@ PowerModifier, ) from qiskit.transpiler.passes import OptimizeAnnotated +from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -102,7 +103,9 @@ def test_optimize_definitions(self): qc.h(0) qc.append(gate, [0, 1, 3]) - qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + # Add "swap" to the basis gates to prevent conjugate reduction from replacing + # control-[SWAP] by CX(0,1) -- CCX(1, 0) -- CX(0, 1) + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u", "swap"])(qc) self.assertEqual(qc_optimized[1].operation.definition, expected_qc_def_optimized) def test_do_not_optimize_definitions_without_basis_gates(self): @@ -195,6 +198,227 @@ def test_if_else(self): self.assertEqual(qc_optimized, expected_qc) + def test_conjugate_reduction(self): + """Test conjugate reduction optimization.""" + + # Create a control-annotated operation. + # The definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.z(0) # P + qc_def.s(0) # P + qc_def.cx(0, 4) # P + qc_def.cx(4, 3) # P + qc_def.y(3) # Q + qc_def.cx(3, 0) # Q + qc_def.cx(4, 3) # R + qc_def.cx(0, 4) # R + qc_def.sdg(0) # R + qc_def.z(0) # R + qc_def.cx(0, 1) # R + qc_def.z(5) # P + qc_def.z(5) # R + qc_def.x(2) # Q + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation + qc = QuantumCircuit(8) + qc.cx(0, 2) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + qc.h(0) + qc.z(4) + + qc_keys = qc.count_ops().keys() + self.assertIn("annotated", qc_keys) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # The pass should simplify the gate + qc_optimized_keys = qc_optimized.count_ops().keys() + self.assertIn("optimized", qc_optimized_keys) + self.assertNotIn("annotated", qc_optimized_keys) + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + def test_conjugate_reduction_collection(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (using annotated gate from the previous example). + """ + + # Create a control-annotated operation. + # The definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.z(0) # P + qc_def.s(0) # P + qc_def.cx(0, 4) # P + qc_def.cx(4, 3) # P + qc_def.y(3) # Q + qc_def.cx(3, 0) # Q + qc_def.cx(4, 3) # R + qc_def.cx(0, 4) # R + qc_def.sdg(0) # R + qc_def.z(0) # R + qc_def.cx(0, 1) # R + qc_def.z(5) # P + qc_def.z(5) # R + qc_def.x(2) # Q + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "s": 1, "sdg": 1, "z": 4, "cx": 6}) + + def test_conjugate_reduction_consecutive_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (multiple consecutive gates on the same pair of qubits). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(0, 1) # P + qc_def.cz(1, 2) # Q + qc_def.swap(0, 1) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "swap": 2}) + + def test_conjugate_reduction_chain_of_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (chain of gates). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.cx(1, 2) # P + qc_def.cx(2, 3) # P + qc_def.h(3) # Q + qc_def.cx(2, 3) # R + qc_def.cx(1, 2) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 6}) + + def test_conjugate_reduction_empty_middle(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (with no gates in the middle circuit). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(0, 1) # P + qc_def.cz(1, 2) # P + qc_def.cz(1, 2) # R + qc_def.swap(0, 1) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "cz": 2, "swap": 2}) + + def test_conjugate_reduction_parallel_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (multiple gates in front and back layers). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(2, 3) # P + qc_def.cz(4, 5) # P + qc_def.h(0) # Q + qc_def.h(1) # Q + qc_def.cx(0, 1) # R + qc_def.swap(2, 3) # R + qc_def.cz(4, 5) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "cz": 2, "swap": 2}) + + def test_conjugate_reduction_cswap(self): + """Test conjugate reduction optimization for control-SWAP.""" + + # Create a circuit with a control-annotated swap + qc = QuantumCircuit(3) + qc.append(SwapGate().control(annotated=True), [0, 1, 2]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Swap(0, 1) gets translated to CX(0, 1), CX(1, 0), CX(0, 1). + # The first and the last of the CXs should be detected as inverse of each other. + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2}) + def test_standalone_var(self): """Test that standalone vars work.""" a = expr.Var.new("a", types.Bool()) From 43c065fc1b2b440d6cf8c393b01dd04624733591 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 2 May 2024 20:51:50 +0300 Subject: [PATCH 032/159] Add ElidePermutations pass to optimization level 3 (#12111) * Add ElideSwaps transpiler pass This commit adds a new transpiler pass ElideSwaps which is a logical optimization pass designed to run prior to layout or any other physical embedding steps in the transpiler pipeline. It traverses the DAG in topological order and when a swap gate is encountered it removes that gate and instead permutes the logical qubits for any subsequent gates in the DAG. This will eliminate any swaps in a circuit not caused by routing. Additionally, this pass is added to the preset pass manager for optimization level 3, we can consider adding it to other levels too if we think it makes sense (there is little overhead, and almost 0 if there are no swaps). One thing to note is that this pass does not recurse into control flow blocks at all, it treats them as black box operations. So if any control flow blocks contain swap gates those will not be optimized away. This change was made because mapping the permutation outside and inside any control flow block was larger in scope than what the intent for this pass was. That type of work is better suited for the routing passes which already have to reason about this. * Update tests with optimization level 3 * Pass final layout from ElideSwap to output The new ElideSwap pass is causing an output permutation just as a routing pass would. This information needs to be passed through to the output in case there is no layout or routing run. In those cases the information about the output permutation caused by the swap elision will be lost and doing layout aware operations like Operator.from_circuit() will not be able to reason about the permutation. This commit fixes this by inserting the original layout and qubit mapping into the property set along with the final layout. Then the base pass class and pass manager class are updated to use the original layout as the initial layout if one isn't set. In cases where we run layout and routing the output metadata from those passes will superscede these new metadata fields. * Move pass in opt level 3 earlier in stage and skip with explicit layout This commit fixes 2 issues in the execution of the new ElideSwaps pass as part of optimization level 3. First we were running it too late in the process both after high level synthesis (which isn't relavant yet, but will be soon when this is expanded to elide all permutations not just swaps) and also after reset diagonal before measurement. The second issue is that if the user is specifying to run with a manual layout set we should not run this pass, as it will interfere with the user intent. * Doc and copy paste fixes * Expand test coverage * Update permutation tracking There were 2 issues with the permutation tracking done in an earlier commit. First, it conflicted with the final_layout property set via routing (or internally by the routing done in the combined sabre layout). This was breaking conditional checks in the preset pass manager around embedding. To fix this a different property is used and set as the output final layout if no final layout is set. The second issue was the output layout object was not taking into account a set initial layout which will permute the qubits and cause the output to not be up to date. This is fixed by updating apply layout to apply the initial layout to the elision_final_layout in the property set. * Generalize pass to support PermutationGate too This commit generalizes the pass from just considering swap gates to all permutations (via the PermutationGate class). This enables the pass to elide additional circuit permutations, not just the special case of a swap gate. The pass and module are renamed accordingly to ElidePermutations and elide_permutations respectively. * Fix permutation handling This commit fixes the recently added handling of the PermutationGate so that it correctly is mapping the qubits. The previous iteration of this messed up the mapping logic so it wasn't valid. * Fix formatting * Fix final layout handling for no initial layout * Improve documentation and log a warning if run post layout * Fix final layout handling with no ElideSwaps being run * Fix docs build * Fix release note * Fix typo * Add test for routing and elide permutations * Apply suggestions from code review Co-authored-by: Jim Garrison * Rename test file to test_elide_permutations.py * Apply suggestions from code review Co-authored-by: Kevin Hartman * Fix test import after rebase * fixing failing test cases this should pass CI after merging #12057 * addresses kehas comments - thx * Adding FinalyzeLayouts pass to pull the virtual circuit permutation from ElidePermutations to the final layout * formatting * partial rebase on top of 12057 + tests * also need test_operator for partial rebase * removing from transpiler flow for now; reworking elide tests * also adding permutation gate to the example * also temporarily reverting test_transpiler.py * update to release notes * minor fixes * adding ElidePermutations and FinalizeLayouts to level 3 * fixing tests * release notes * properly merging with main (now that ElidePermutations is merged) * and also deleting finalize_layouts * Update releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml * updating code comment * Fixing failing test: now that ElidePermutations pass runs with optimization level 3, it does remove the SWAP gate. So we need to consider optimization_level=1 to assert that the extra pass does not remove SWAP gates --------- Co-authored-by: Matthew Treinish Co-authored-by: Jim Garrison Co-authored-by: Kevin Hartman Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> --- .../preset_passmanagers/builtin_plugins.py | 4 ++-- ...ermutations-to-pipeline-077dad03bd55ab9c.yaml | 9 +++++++++ test/python/compiler/test_transpiler.py | 16 ++++++++++++---- .../python/transpiler/test_elide_permutations.py | 3 ++- 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 8eddb9adf63..f85b4d113c1 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -26,7 +26,7 @@ from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import CheckMap from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements -from qiskit.transpiler.passes import OptimizeSwapBeforeMeasure +from qiskit.transpiler.passes import ElidePermutations from qiskit.transpiler.passes import RemoveDiagonalGatesBeforeMeasure from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.preset_passmanagers.plugin import ( @@ -133,7 +133,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.unitary_synthesis_plugin_config, pass_manager_config.hls_config, ) - init.append(OptimizeSwapBeforeMeasure()) + init.append(ElidePermutations()) init.append(RemoveDiagonalGatesBeforeMeasure()) init.append( InverseCancellation( diff --git a/releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml b/releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml new file mode 100644 index 00000000000..ddc35ddcb98 --- /dev/null +++ b/releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The transpiler pass :class:`~.ElidePermutations` + runs by default with optimization level 2 and 3. Intuitively, removing + :class:`~.SwapGate`\s and :class:`~qiskit.circuit.library.PermutationGate`\s + in a virtual circuit is almost always beneficial, as it makes the circuit shorter + and easier to route. As :class:`~.OptimizeSwapBeforeMeasure` is a special case + of :class:`~.ElidePermutations`, it has been removed from optimization level 3. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 2a8c86b27ab..6166528f886 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1750,8 +1750,11 @@ def test_translate_ecr_basis(self, optimization_level): optimization_level=optimization_level, seed_transpiler=42, ) - self.assertEqual(res.count_ops()["ecr"], 9) - self.assertTrue(Operator(res).equiv(circuit)) + + # Swap gates get optimized away in opt. level 2, 3 + expected_num_ecr_gates = 6 if optimization_level in (2, 3) else 9 + self.assertEqual(res.count_ops()["ecr"], expected_num_ecr_gates) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) def test_optimize_ecr_basis(self): """Test highest optimization level can optimize over ECR.""" @@ -1760,8 +1763,13 @@ def test_optimize_ecr_basis(self): circuit.iswap(0, 1) res = transpile(circuit, basis_gates=["u", "ecr"], optimization_level=3, seed_transpiler=42) - self.assertEqual(res.count_ops()["ecr"], 1) - self.assertTrue(Operator(res).equiv(circuit)) + + # an iswap gate is equivalent to (swap, CZ) up to single-qubit rotations. Normally, the swap gate + # in the circuit would cancel with the swap gate of the (swap, CZ), leaving a single CZ gate that + # can be realized via one ECR gate. However, with the introduction of ElideSwap, the swap gate + # cancellation can not occur anymore, thus requiring two ECR gates for the iswap gate. + self.assertEqual(res.count_ops()["ecr"], 2) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) def test_approximation_degree_invalid(self): """Test invalid approximation degree raises.""" diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py index d3d807834fc..6e0379f4bc5 100644 --- a/test/python/transpiler/test_elide_permutations.py +++ b/test/python/transpiler/test_elide_permutations.py @@ -304,6 +304,7 @@ class TestElidePermutationsInTranspileFlow(QiskitTestCase): def test_not_run_after_layout(self): """Test ElidePermutations doesn't do anything after layout.""" + qc = QuantumCircuit(3) qc.h(0) qc.swap(0, 2) @@ -312,7 +313,7 @@ def test_not_run_after_layout(self): qc.h(1) spm = generate_preset_pass_manager( - optimization_level=3, initial_layout=list(range(2, -1, -1)), seed_transpiler=42 + optimization_level=1, initial_layout=list(range(2, -1, -1)), seed_transpiler=42 ) spm.layout += ElidePermutations() res = spm.run(qc) From 849fa0066d6a7bd299654c163e8ebedd1914f746 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Fri, 3 May 2024 00:08:49 +0400 Subject: [PATCH 033/159] Add SamplerPubResult (#12143) * add SamplerPubResult * add join_data * Update qiskit/primitives/containers/sampler_pub_result.py Co-authored-by: Ian Hincks * adding tests * add join_data tests * add reno * fix linting --------- Co-authored-by: Ian Hincks Co-authored-by: Ian Hincks --- qiskit/primitives/backend_sampler_v2.py | 12 +- qiskit/primitives/base/base_sampler.py | 4 +- qiskit/primitives/containers/__init__.py | 9 +- qiskit/primitives/containers/pub_result.py | 2 +- .../containers/sampler_pub_result.py | 74 +++++++++++++ qiskit/primitives/statevector_sampler.py | 12 +- .../sampler-pub-result-e64e7de1bae2d35e.yaml | 17 +++ .../containers/test_sampler_pub_result.py | 104 ++++++++++++++++++ 8 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 qiskit/primitives/containers/sampler_pub_result.py create mode 100644 releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml create mode 100644 test/python/primitives/containers/test_sampler_pub_result.py diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index 3b560645af6..ff7a32580fe 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -29,8 +29,8 @@ BitArray, DataBin, PrimitiveResult, - PubResult, SamplerPubLike, + SamplerPubResult, ) from qiskit.primitives.containers.bit_array import _min_num_bytes from qiskit.primitives.containers.sampler_pub import SamplerPub @@ -124,7 +124,7 @@ def options(self) -> Options: def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> PrimitiveJob[PrimitiveResult[PubResult]]: + ) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]: if shots is None: shots = self._options.default_shots coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] @@ -142,7 +142,7 @@ def _validate_pubs(self, pubs: list[SamplerPub]): UserWarning, ) - def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[PubResult]: + def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: pub_dict = defaultdict(list) # consolidate pubs with the same number of shots for i, pub in enumerate(pubs): @@ -157,7 +157,7 @@ def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[PubResult]: results[i] = pub_result return PrimitiveResult(results) - def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[PubResult]: + def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]: """Compute results for pubs that all require the same value of ``shots``.""" # prepare circuits bound_circuits = [pub.parameter_values.bind_all(pub.circuit) for pub in pubs] @@ -197,7 +197,7 @@ def _postprocess_pub( shape: tuple[int, ...], meas_info: list[_MeasureInfo], max_num_bytes: int, - ) -> PubResult: + ) -> SamplerPubResult: """Converts the memory data into an array of bit arrays with the shape of the pub.""" arrays = { item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8) @@ -213,7 +213,7 @@ def _postprocess_pub( meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - return PubResult(DataBin(**meas, shape=shape), metadata={}) + return SamplerPubResult(DataBin(**meas, shape=shape), metadata={}) def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 1d071a15728..94c9a7681d0 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -23,8 +23,8 @@ from qiskit.providers import JobV1 as Job from ..containers.primitive_result import PrimitiveResult -from ..containers.pub_result import PubResult from ..containers.sampler_pub import SamplerPubLike +from ..containers.sampler_pub_result import SamplerPubResult from . import validation from .base_primitive import BasePrimitive from .base_primitive_job import BasePrimitiveJob @@ -165,7 +165,7 @@ class BaseSamplerV2(ABC): @abstractmethod def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: + ) -> BasePrimitiveJob[PrimitiveResult[SamplerPubResult]]: """Run and collect samples from each pub. Args: diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 63c9c600de1..62fb49a3fb9 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -15,11 +15,12 @@ """ +from .bindings_array import BindingsArrayLike from .bit_array import BitArray -from .data_bin import make_data_bin, DataBin +from .data_bin import DataBin, make_data_bin +from .estimator_pub import EstimatorPubLike +from .observables_array import ObservableLike, ObservablesArrayLike from .primitive_result import PrimitiveResult from .pub_result import PubResult -from .estimator_pub import EstimatorPubLike from .sampler_pub import SamplerPubLike -from .bindings_array import BindingsArrayLike -from .observables_array import ObservableLike, ObservablesArrayLike +from .sampler_pub_result import SamplerPubResult diff --git a/qiskit/primitives/containers/pub_result.py b/qiskit/primitives/containers/pub_result.py index 369179e4629..1facb850ade 100644 --- a/qiskit/primitives/containers/pub_result.py +++ b/qiskit/primitives/containers/pub_result.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ -Base Pub class +Base Pub result class """ from __future__ import annotations diff --git a/qiskit/primitives/containers/sampler_pub_result.py b/qiskit/primitives/containers/sampler_pub_result.py new file mode 100644 index 00000000000..7a2d2a9e3fc --- /dev/null +++ b/qiskit/primitives/containers/sampler_pub_result.py @@ -0,0 +1,74 @@ +# 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. + +""" +Sampler Pub result class +""" + +from __future__ import annotations + +from typing import Iterable + +import numpy as np + +from .bit_array import BitArray +from .pub_result import PubResult + + +class SamplerPubResult(PubResult): + """Result of Sampler Pub.""" + + def join_data(self, names: Iterable[str] | None = None) -> BitArray | np.ndarray: + """Join data from many registers into one data container. + + Data is joined along the bits axis. For example, for :class:`~.BitArray` data, this corresponds + to bitstring concatenation. + + Args: + names: Which registers to join. Their order is maintained, for example, given + ``["alpha", "beta"]``, the data from register ``alpha`` is placed to the left of the + data from register ``beta``. When ``None`` is given, this value is set to the + ordered list of register names, which will have been preserved from the input circuit + order. + + Returns: + Joint data. + + Raises: + ValueError: If specified names are empty. + ValueError: If specified name does not exist. + TypeError: If specified data comes from incompatible types. + """ + if names is None: + names = list(self.data) + if not names: + raise ValueError("No entry exists in the data bin.") + else: + names = list(names) + if not names: + raise ValueError("An empty name list is given.") + for name in names: + if name not in self.data: + raise ValueError(f"Name '{name}' does not exist.") + + data = [self.data[name] for name in names] + if isinstance(data[0], BitArray): + if not all(isinstance(datum, BitArray) for datum in data): + raise TypeError("Data comes from incompatible types.") + joint_data = BitArray.concatenate_bits(data) + elif isinstance(data[0], np.ndarray): + if not all(isinstance(datum, np.ndarray) for datum in data): + raise TypeError("Data comes from incompatible types.") + joint_data = np.concatenate(data, axis=-1) + else: + raise TypeError("Data comes from incompatible types.") + return joint_data diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index c78865aec7e..90fe452ad12 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -15,9 +15,9 @@ from __future__ import annotations +import warnings from dataclasses import dataclass from typing import Iterable -import warnings import numpy as np from numpy.typing import NDArray @@ -32,7 +32,7 @@ BitArray, DataBin, PrimitiveResult, - PubResult, + SamplerPubResult, SamplerPubLike, ) from .containers.sampler_pub import SamplerPub @@ -154,7 +154,7 @@ def seed(self) -> np.random.Generator | int | None: def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> PrimitiveJob[PrimitiveResult[PubResult]]: + ) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]: if shots is None: shots = self._default_shots coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] @@ -169,11 +169,11 @@ def run( job._submit() return job - def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: + def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: results = [self._run_pub(pub) for pub in pubs] return PrimitiveResult(results) - def _run_pub(self, pub: SamplerPub) -> PubResult: + def _run_pub(self, pub: SamplerPub) -> SamplerPubResult: circuit, qargs, meas_info = _preprocess_circuit(pub.circuit) bound_circuits = pub.parameter_values.bind_all(circuit) arrays = { @@ -197,7 +197,7 @@ def _run_pub(self, pub: SamplerPub) -> PubResult: meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - return PubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots}) + return SamplerPubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots}) def _preprocess_circuit(circuit: QuantumCircuit): diff --git a/releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml b/releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml new file mode 100644 index 00000000000..2c5c2a6e10c --- /dev/null +++ b/releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml @@ -0,0 +1,17 @@ +--- +features_primitives: + - | + The subclass :class:`~.SamplerPubResult` of :class:`~.PubResult` was added, + which :class:`~.BaseSamplerV2` implementations can return. The main feature + added in this new subclass is :meth:`~.SamplerPubResult.join_data`, which + joins together (a subset of) the contents of :attr:`~.PubResult.data` into + a single object. This enables the following patterns: + + .. code:: python + + job_result = sampler.run([pub1, pub2, pub3], shots=123).result() + + # assuming all returned data entries are BitArrays + counts1 = job_result[0].join_data().get_counts() + bistrings2 = job_result[1].join_data().get_bitstrings() + array3 = job_result[2].join_data().array \ No newline at end of file diff --git a/test/python/primitives/containers/test_sampler_pub_result.py b/test/python/primitives/containers/test_sampler_pub_result.py new file mode 100644 index 00000000000..fe7144a0741 --- /dev/null +++ b/test/python/primitives/containers/test_sampler_pub_result.py @@ -0,0 +1,104 @@ +# 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. + + +"""Unit tests for SamplerPubResult.""" + +from test import QiskitTestCase + +import numpy as np + +from qiskit.primitives.containers import BitArray, DataBin, SamplerPubResult + + +class SamplerPubResultCase(QiskitTestCase): + """Test the SamplerPubResult class.""" + + def test_construction(self): + """Test that the constructor works.""" + ba = BitArray.from_samples(["00", "11"], 2) + counts = {"00": 1, "11": 1} + data_bin = DataBin(a=ba, b=ba) + pub_result = SamplerPubResult(data_bin) + self.assertEqual(pub_result.data.a.get_counts(), counts) + self.assertEqual(pub_result.data.b.get_counts(), counts) + self.assertEqual(pub_result.metadata, {}) + + pub_result = SamplerPubResult(data_bin, {"x": 1}) + self.assertEqual(pub_result.data.a.get_counts(), counts) + self.assertEqual(pub_result.data.b.get_counts(), counts) + self.assertEqual(pub_result.metadata, {"x": 1}) + + def test_repr(self): + """Test that the repr doesn't fail""" + # we are primarily interested in making sure some future change doesn't cause the repr to + # raise an error. it is more sensible for humans to detect a deficiency in the formatting + # itself, should one be uncovered + ba = BitArray.from_samples(["00", "11"], 2) + data_bin = DataBin(a=ba, b=ba) + self.assertTrue(repr(SamplerPubResult(data_bin)).startswith("SamplerPubResult")) + self.assertTrue(repr(SamplerPubResult(data_bin, {"x": 1})).startswith("SamplerPubResult")) + + def test_join_data_failures(self): + """Test the join_data() failure mechanisms work.""" + + result = SamplerPubResult(DataBin()) + with self.assertRaisesRegex(ValueError, "No entry exists in the data bin"): + result.join_data() + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(ValueError, "An empty name list is given"): + result.join_data([]) + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(ValueError, "Name 'foo' does not exist"): + result.join_data(["alpha", "foo"]) + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = np.empty((2,)) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + alpha = np.empty((2,)) + beta = BitArray.from_samples(["00", "11"], 2) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + result = SamplerPubResult(DataBin(alpha=1, beta={})) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + def test_join_data_bit_array_default(self): + """Test the join_data() method with no arguments and bit arrays.""" + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + data_bin = DataBin(alpha=alpha, beta=beta) + result = SamplerPubResult(data_bin) + + gamma = result.join_data() + self.assertEqual(list(gamma.get_bitstrings()), ["01000", "10111"]) + + def test_join_data_ndarray_default(self): + """Test the join_data() method with no arguments and ndarrays.""" + alpha = np.linspace(0, 1, 30).reshape((2, 3, 5)) + beta = np.linspace(0, 1, 12).reshape((2, 3, 2)) + data_bin = DataBin(alpha=alpha, beta=beta, shape=(2, 3)) + result = SamplerPubResult(data_bin) + + gamma = result.join_data() + np.testing.assert_allclose(gamma, np.concatenate([alpha, beta], axis=2)) From cc7c47dc7b435eee9055cd2ec41f3e0b28481b15 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 06:32:37 -0400 Subject: [PATCH 034/159] Fix composition of multiple virtual permutation layouts (#12335) This commit fixes the composition of multiple virtual permutation layouts in StarPreRouting and ElidePermutations. Both of these passes set a virtual permutation layout and if both running a pipeline the layouts need to be composed together to track the permutation. Previously the logic for doing this was incorrect (and also had a syntax error). This commit fixes this so that you can run the two passes together (along with any other future passes that do a similar thing). This was spun out from #12196 as a standalone bugfix. --- .../passes/optimization/elide_permutations.py | 6 ++-- .../passes/routing/star_prerouting.py | 6 ++-- .../transpiler/test_elide_permutations.py | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/elide_permutations.py b/qiskit/transpiler/passes/optimization/elide_permutations.py index d9704ff0518..ca6902f15b4 100644 --- a/qiskit/transpiler/passes/optimization/elide_permutations.py +++ b/qiskit/transpiler/passes/optimization/elide_permutations.py @@ -105,8 +105,10 @@ def _apply_mapping(qargs): self.property_set["original_qubit_indices"] = input_qubit_mapping new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) - if current_layout := self.property_set["virtual_permutation_layout"] is not None: - self.property_set["virtual_permutation_layout"] = current_layout.compose(new_layout) + if current_layout := self.property_set["virtual_permutation_layout"]: + self.property_set["virtual_permutation_layout"] = new_layout.compose( + current_layout.inverse(dag.qubits, dag.qubits), dag.qubits + ) else: self.property_set["virtual_permutation_layout"] = new_layout return new_dag diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index 8e278471295..4b27749c6da 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -267,8 +267,10 @@ def run(self, dag): self.property_set["original_qubit_indices"] = input_qubit_mapping new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) - if current_layout := self.property_set["virtual_permutation_layout"] is not None: - self.property_set["virtual_permutation_layout"] = current_layout.compose(new_layout) + if current_layout := self.property_set["virtual_permutation_layout"]: + self.property_set["virtual_permutation_layout"] = new_layout.compose( + current_layout.inverse(dag.qubits, dag.qubits), dag.qubits + ) else: self.property_set["virtual_permutation_layout"] = new_layout diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py index 6e0379f4bc5..c96b5ca32d8 100644 --- a/test/python/transpiler/test_elide_permutations.py +++ b/test/python/transpiler/test_elide_permutations.py @@ -17,6 +17,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.library.generalized_gates import PermutationGate from qiskit.transpiler.passes.optimization.elide_permutations import ElidePermutations +from qiskit.transpiler.passes.routing import StarPreRouting from qiskit.circuit.controlflow import IfElseOp from qiskit.quantum_info import Operator from qiskit.transpiler.coupling import CouplingMap @@ -426,6 +427,33 @@ def test_unitary_equivalence_routing_and_basis_translation(self): qc_with_ancillas.append(qc, [0, 1, 2, 3, 4]) self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc_with_ancillas))) + def test_unitary_equivalence_virtual_permutation_layout_composition(self): + """Test on a larger example that includes routing and basis translation.""" + + qc = QuantumCircuit(5) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.append(PermutationGate([0, 2, 1]), [0, 1, 2]) + qc.h(1) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + coupling_map=CouplingMap.from_line(5), + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + spm.init += StarPreRouting() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + if __name__ == "__main__": unittest.main() From 68f4b528948ddefd874a754ac8db6865ea964bb5 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Fri, 3 May 2024 09:49:35 -0400 Subject: [PATCH 035/159] Adapt crates/qasm3 to work with recent versions of openqasm3_parser (#12087) * Adapt crates/qasm3 to work with recent versions of openqasm3_parser This commit adds no new features or capabilities to the importer. But it brings the importer up to date with the latest version of openqasm3_parser as a preliminary step in improving the importer. The biggest change in openqasm3_parser is in handling stdgates.inc. Upon encountering `include "stdgates.inc" openqasm3_parser reads no file, but rather adds symbols to the symbol table for gates in the standard library. The function `convert_asg` in the importer calls oq3_semantics::symbols::SymbolTable.gates which returns a `Vec` of information about each "declared" gate. The information is the gate's name and signature and SymbolID, which is sufficient to do everything the importer could do before this commit. Encountering `Stmt::GateDefinition` now is a no-op rather than an error. (This was previously called `Stmt::GateDeclaration`, but importantly it is really a definition.) How the standard library, and gates in general, are handled will continue to evolve. A behavioral difference between this commit and its parent: Before if the importer encountered a gate call before the corresponding definition an error would be raised. With the current commit, the importer behaves as if all gate definitions were moved to the top of the OQ3 program. However, the error will still be found by the parser so that the importer never will begin processing statements. The importer depends on a particular branch of a copy of openqasm3_parser. When the commit is ready to merge, we will release a version of openqasm3_parser and depend instead on that release. See https://github.com/openqasm/openqasm/pull/517 * Remove unnecessary conversion `name.to_string()` Response to reviewer comment https://github.com/Qiskit/qiskit/pull/12087/files#r1586949717 * Remove check for U gate in map_gate_ids * This requires modifying the external parser crate. * Depend temporarily on the branch of the parser with this modification. * Make another change required by other upstream improvments. * Cargo config files are changed because of above changes. * Return error returned by map_gate_ids in convert_asg Previously this error was ignored. This would almost certainly cause an error later. But better to do it at the correct place. * Remove superfluous call to `iter()` * Depend on published oq3_semantics 0.6.0 * Fix cargo lock file --------- Co-authored-by: Matthew Treinish --- Cargo.lock | 216 ++++++++++++++++---------------------- crates/qasm3/Cargo.toml | 2 +- crates/qasm3/src/build.rs | 84 ++++++++------- crates/qasm3/src/expr.rs | 4 +- 4 files changed, 134 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dee434e870a..50903b3811a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -106,7 +117,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -265,7 +276,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -285,7 +296,7 @@ checksum = "60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -509,14 +520,17 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", "rayon", ] @@ -550,7 +564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "rayon", ] @@ -606,9 +620,9 @@ checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -618,9 +632,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -771,9 +785,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e867d2797100b8068715e26566a5567c598424d7eddf7118c6b38bc3b15633" +checksum = "0de2f0f9d48042c12f82b2550808378718627e108fc3f6adf63e02e5293541a3" dependencies = [ "unicode-properties", "unicode-xid", @@ -781,9 +795,9 @@ dependencies = [ [[package]] name = "oq3_parser" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf260dea71b56b405d091d476748c1f9b0a4d22b4ec9af416e002e2df25613f9" +checksum = "e69b215426a4a2a023fd62cca4436c633ba0ab39ee260aca875ac60321b3704b" dependencies = [ "drop_bomb", "oq3_lexer", @@ -792,12 +806,12 @@ dependencies = [ [[package]] name = "oq3_semantics" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ba220b91ff849190d53b296711774f761b7e06744b16a9c8f19fc2fb37de47" +checksum = "3e15e9cee54e92fb1b3aaa42556b0bd76c8c1c10912a7d6798f43dfc3afdcb0d" dependencies = [ "boolenum", - "hashbrown 0.14.3", + "hashbrown 0.12.3", "oq3_source_file", "oq3_syntax", "rowan", @@ -805,9 +819,9 @@ dependencies = [ [[package]] name = "oq3_source_file" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a81fd0c1c100ad8d7a23711c897791d693c3f5b1f3d044cb8c5770766f819c" +checksum = "4f65243cc4807c600c544a21db6c17544c53aa2bc034b3eccf388251cc6530e7" dependencies = [ "ariadne", "oq3_syntax", @@ -815,9 +829,9 @@ dependencies = [ [[package]] name = "oq3_syntax" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7da2ef9a591d77eee43e972e79fc95c218545e5e79b93738d20479d8d7627ec" +checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" dependencies = [ "cov-mark", "either", @@ -829,6 +843,7 @@ dependencies = [ "ra_ap_stdx", "rowan", "rustc-hash", + "rustversion", "smol_str", "triomphe", "xshell", @@ -836,9 +851,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -846,15 +861,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -865,9 +880,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -876,9 +891,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -886,22 +901,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "pest_meta" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -994,7 +1009,7 @@ checksum = "d315b3197b780e4873bc0e11251cb56a33f65a6032a3d39b8d1405c255513766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1017,7 +1032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "indoc", "libc", @@ -1062,7 +1077,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1075,18 +1090,18 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "qiskit-accelerate" version = "1.1.0" dependencies = [ - "ahash", + "ahash 0.8.11", "approx", "faer", "faer-ext", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "itertools 0.12.1", "ndarray", @@ -1109,7 +1124,7 @@ dependencies = [ name = "qiskit-circuit" version = "1.1.0" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pyo3", ] @@ -1128,7 +1143,7 @@ dependencies = [ name = "qiskit-qasm2" version = "1.1.0" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pyo3", "qiskit-circuit", ] @@ -1137,7 +1152,7 @@ dependencies = [ name = "qiskit-qasm3" version = "1.1.0" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "oq3_semantics", "pyo3", @@ -1275,11 +1290,11 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -1289,7 +1304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" dependencies = [ "countme", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "memoffset", "rustc-hash", "text-size", @@ -1301,15 +1316,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + [[package]] name = "rustworkx-core" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" dependencies = [ - "ahash", + "ahash 0.8.11", "fixedbitset", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "num-traits", "petgraph", @@ -1343,22 +1364,22 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1400,9 +1421,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -1437,22 +1458,22 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1487,9 +1508,9 @@ checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -1543,11 +1564,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1604,21 +1625,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.5" @@ -1641,12 +1647,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" @@ -1659,12 +1659,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" @@ -1677,12 +1671,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1701,12 +1689,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" @@ -1719,12 +1701,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" @@ -1737,12 +1713,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" @@ -1755,12 +1725,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" @@ -1805,5 +1769,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index a8e20d13d58..4dd0d977786 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -13,4 +13,4 @@ doctest = false pyo3.workspace = true indexmap.workspace = true hashbrown.workspace = true -oq3_semantics = "0.0.7" +oq3_semantics = "0.6.0" diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 154cd391252..2f817187625 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -226,42 +226,32 @@ impl BuilderState { self.qc.append(py, instruction).map(|_| ()) } - fn define_gate( - &mut self, - _py: Python, - ast_symbols: &SymbolTable, - decl: &asg::GateDeclaration, - ) -> PyResult<()> { - let name_id = decl - .name() - .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; - let name_symbol = &ast_symbols[name_id]; - let pygate = self.pygates.get(name_symbol.name()).ok_or_else(|| { - QASM3ImporterError::new_err(format!( - "can't handle non-built-in gate: '{}'", - name_symbol.name() - )) - })?; - let defined_num_params = decl.params().as_ref().map_or(0, Vec::len); - let defined_num_qubits = decl.qubits().len(); - if pygate.num_params() != defined_num_params { - return Err(QASM3ImporterError::new_err(format!( - "given constructor for '{}' expects {} parameters, but is defined as taking {}", - pygate.name(), - pygate.num_params(), - defined_num_params, - ))); - } - if pygate.num_qubits() != defined_num_qubits { - return Err(QASM3ImporterError::new_err(format!( - "given constructor for '{}' expects {} qubits, but is defined as taking {}", - pygate.name(), - pygate.num_qubits(), - defined_num_qubits, - ))); + // Map gates in the symbol table to Qiskit gates in the standard library. + // Encountering any gates not in the standard library results in raising an exception. + // Gates mapped via CustomGates will not raise an exception. + fn map_gate_ids(&mut self, _py: Python, ast_symbols: &SymbolTable) -> PyResult<()> { + for (name, name_id, defined_num_params, defined_num_qubits) in ast_symbols.gates() { + let pygate = self.pygates.get(name).ok_or_else(|| { + QASM3ImporterError::new_err(format!("can't handle non-built-in gate: '{}'", name)) + })?; + if pygate.num_params() != defined_num_params { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} parameters, but is defined as taking {}", + pygate.name(), + pygate.num_params(), + defined_num_params, + ))); + } + if pygate.num_qubits() != defined_num_qubits { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} qubits, but is defined as taking {}", + pygate.name(), + pygate.num_qubits(), + defined_num_qubits, + ))); + } + self.symbols.gates.insert(name_id.clone(), pygate.clone()); } - self.symbols.gates.insert(name_id.clone(), pygate.clone()); Ok(()) } @@ -377,37 +367,45 @@ pub fn convert_asg( pygates: gate_constructors, module, }; + + state.map_gate_ids(py, ast_symbols)?; + for statement in program.stmts().iter() { match statement { asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, asg::Stmt::DeclareClassical(decl) => state.declare_classical(py, ast_symbols, decl)?, asg::Stmt::DeclareQuantum(decl) => state.declare_quantum(py, ast_symbols, decl)?, - asg::Stmt::GateDeclaration(decl) => state.define_gate(py, ast_symbols, decl)?, + // We ignore gate definitions because the only information we can currently use + // from them is extracted with `SymbolTable::gates` via `map_gate_ids`. + asg::Stmt::GateDefinition(_) => (), asg::Stmt::Barrier(barrier) => state.apply_barrier(py, ast_symbols, barrier)?, asg::Stmt::Assignment(assignment) => state.assign(py, ast_symbols, assignment)?, - asg::Stmt::Alias + asg::Stmt::Alias(_) | asg::Stmt::AnnotatedStmt(_) | asg::Stmt::Block(_) | asg::Stmt::Box | asg::Stmt::Break | asg::Stmt::Cal | asg::Stmt::Continue - | asg::Stmt::Def + | asg::Stmt::DeclareHardwareQubit(_) | asg::Stmt::DefCal - | asg::Stmt::Delay + | asg::Stmt::DefStmt(_) + | asg::Stmt::Delay(_) | asg::Stmt::End | asg::Stmt::ExprStmt(_) | asg::Stmt::Extern - | asg::Stmt::For + | asg::Stmt::ForStmt(_) | asg::Stmt::GPhaseCall(_) - | asg::Stmt::IODeclaration | asg::Stmt::If(_) | asg::Stmt::Include(_) + | asg::Stmt::InputDeclaration(_) + | asg::Stmt::ModifiedGPhaseCall(_) | asg::Stmt::NullStmt | asg::Stmt::OldStyleDeclaration + | asg::Stmt::OutputDeclaration(_) | asg::Stmt::Pragma(_) - | asg::Stmt::Reset - | asg::Stmt::Return + | asg::Stmt::Reset(_) + | asg::Stmt::SwitchCaseStmt(_) | asg::Stmt::While(_) => { return Err(QASM3ImporterError::new_err(format!( "this statement is not yet handled during OpenQASM 3 import: {:?}", diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index d16bd53add0..e912aecdb87 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -244,11 +244,11 @@ pub fn eval_qarg( qarg: &asg::GateOperand, ) -> PyResult { match qarg { - asg::GateOperand::Identifier(iden) => broadcast_bits_for_identifier( + asg::GateOperand::Identifier(symbol) => broadcast_bits_for_identifier( py, &our_symbols.qubits, &our_symbols.qregs, - iden.symbol().as_ref().unwrap(), + symbol.as_ref().unwrap(), ), asg::GateOperand::IndexedIdentifier(indexed) => { let iden_symbol = indexed.identifier().as_ref().unwrap(); From 7882eb108ba935585d96b808ac12c916790a8a5b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 09:50:46 -0400 Subject: [PATCH 036/159] Set version number to 1.1.0rc1 for first release candidate (#12328) For the 1.1.0 release we're going to push release candidates prior to the final to enable testing before we cut the final release. In preparation for tagging the first release candidate this commit updates the version string to indicate it's a release candidate. This commit should be the final commit on main for 1.1.0rc1 and what gets tagged as 1.1.0rc1 post-merge. --- docs/conf.py | 4 ++-- qiskit/VERSION.txt | 2 +- .../abstract-commutation-analysis-3518129e91a33599.yaml | 0 .../add-annotated-arg-to-power-4afe90e89fa50f5a.yaml | 0 .../{ => 1.1}/add-backend-estimator-v2-26cf14a3612bb81a.yaml | 0 .../{ => 1.1}/add-backend-sampler-v2-5e40135781eebc7f.yaml | 0 .../{ => 1.1}/add-bitarray-utilities-c85261138d5a1a97.yaml | 0 .../add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml | 0 .../add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml | 0 .../notes/{ => 1.1}/add-elide-swaps-b0a4c373c9af1efd.yaml | 0 .../{ => 1.1}/add-linear-plugin-options-b8a0ffe70dfe1676.yaml | 0 .../add-run-all-plugins-option-ba8806a269e5713c.yaml | 0 .../{ => 1.1}/add-scheduler-warnings-da6968a39fd8e6e7.yaml | 0 ...-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml | 0 .../added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml | 0 .../notes/{ => 1.1}/classical-store-e64ee1286219a862.yaml | 0 .../{ => 1.1}/commutation-checker-utf8-47b13b78a40af196.yaml | 0 ...tive-cancellation-preset-passmanager-c137ce516a10eae5.yaml | 0 .../{ => 1.1}/databin-construction-72ec041075410cb2.yaml | 0 .../notes/{ => 1.1}/databin-mapping-45d24d71f9bb4eda.yaml | 0 .../notes/{ => 1.1}/deprecate-3.8-a9db071fa3c85b1a.yaml | 0 .../{ => 1.1}/deprecate_providerV1-ba17d7b4639d1cc5.yaml | 0 .../notes/{ => 1.1}/expr-bitshift-index-e9cfc6ea8729ef5e.yaml | 0 .../notes/{ => 1.1}/faster-lie-trotter-ba8f6dd84fe4cae4.yaml | 0 .../fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml | 0 .../fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml | 0 .../fix-control-flow-fold-minus-one-f2af168a5313385f.yaml | 0 .../fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml | 0 .../fix-custom-transpile-constraints-5defa36d540d1608.yaml | 0 .../{ => 1.1}/fix-equivalence-setentry-5a30b0790666fcf2.yaml | 0 ...ix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml | 0 .../fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml | 0 ...ix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml | 0 .../{ => 1.1}/fix-mcx-mcp-performance-b00040804b47b200.yaml | 0 .../fix-missing-qubit-properties-35137aa5250d9368.yaml | 0 .../{ => 1.1}/fix-passmanager-reuse-151877e1905d49df.yaml | 0 .../notes/{ => 1.1}/fix-pauli-evolve-ecr-and-name-bugs.yaml | 0 ...fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml | 0 .../notes/{ => 1.1}/fix-pub-coerce-5d13700e15126421.yaml | 0 .../fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml | 0 .../fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml | 0 .../{ => 1.1}/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml | 0 .../{ => 1.1}/fix-scheduling-units-59477912b47d3dc1.yaml | 0 ...x-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml | 0 .../notes/{ => 1.1}/fix_soft_compare-3f4148aab3a4606b.yaml | 0 .../notes/{ => 1.1}/fixes_10852-e197344c5f44b4f1.yaml | 0 .../notes/{ => 1.1}/fixes_11212-d6de3c007ce6d697.yaml | 0 .../notes/{ => 1.1}/followup_11468-61c6181e62531796.yaml | 0 .../notes/{ => 1.1}/histogram-style-03807965c3cc2e8a.yaml | 0 .../notes/{ => 1.1}/layout-compose-0b9a9a72359638d8.yaml | 0 .../notes/{ => 1.1}/macos-arm64-tier-1-c5030f009be6adcb.yaml | 0 .../notes/{ => 1.1}/nlocal-perf-3b8ebd9be1b2f4b3.yaml | 0 releasenotes/notes/{ => 1.1}/numpy-2.0-2f3e35bd42c48518.yaml | 0 .../notes/{ => 1.1}/obs-array-coerce-0d-28b192fb3d004d4a.yaml | 0 .../operator-from-circuit-bugfix-5dab5993526a2b0a.yaml | 0 .../notes/{ => 1.1}/optimization-level2-2c8c1488173aed31.yaml | 0 ...timize-annotated-conjugate-reduction-656438d3642f27dc.yaml | 0 .../notes/{ => 1.1}/parameter-hash-eq-645f9de55aa78d02.yaml | 0 ...signment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml | 0 .../notes/{ => 1.1}/pauli-apply-layout-cdcbc1bce724a150.yaml | 0 .../public-noncommutation-graph-dd31c931b7045a4f.yaml | 0 ..._manager_compat_with_ParameterVector-7d31395fd4019827.yaml | 0 .../qasm3-parameter-gate-clash-34ef7b0383849a78.yaml | 0 .../qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml | 0 .../quantumcircuit-append-copy-8a9b71ad4b789490.yaml | 0 releasenotes/notes/{ => 1.1}/qv-perf-be76290f472e4777.yaml | 0 .../notes/{ => 1.1}/remove-final-reset-488247c01c4e147d.yaml | 0 .../{ => 1.1}/removed_deprecated_0.21-741d08a01a7ed527.yaml | 0 .../{ => 1.1}/reverse-permutation-lnn-409a07c7f6d0eed9.yaml | 0 .../rework-inst-durations-passes-28c78401682e22c0.yaml | 0 .../rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml | 0 .../notes/{ => 1.1}/rust-two-qubit-weyl-ec551f3f9c812124.yaml | 0 .../notes/{ => 1.1}/sampler-pub-result-e64e7de1bae2d35e.yaml | 0 .../show_idle_and_show_barrier-6e77e1f9d6f55599.yaml | 0 .../notes/{ => 1.1}/spo-to-matrix-26445a791e24f62a.yaml | 0 .../notes/{ => 1.1}/star-prerouting-0998b59880c20cef.yaml | 0 .../{ => 1.1}/update-gate-dictionary-c0c017be67bb2f29.yaml | 0 .../{ => 1.1}/use-target-in-transpile-7c04b14549a11f40.yaml | 0 78 files changed, 3 insertions(+), 3 deletions(-) rename releasenotes/notes/{ => 1.1}/abstract-commutation-analysis-3518129e91a33599.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-backend-estimator-v2-26cf14a3612bb81a.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-backend-sampler-v2-5e40135781eebc7f.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-bitarray-utilities-c85261138d5a1a97.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-elide-swaps-b0a4c373c9af1efd.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-linear-plugin-options-b8a0ffe70dfe1676.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-run-all-plugins-option-ba8806a269e5713c.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-scheduler-warnings-da6968a39fd8e6e7.yaml (100%) rename releasenotes/notes/{ => 1.1}/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml (100%) rename releasenotes/notes/{ => 1.1}/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml (100%) rename releasenotes/notes/{ => 1.1}/classical-store-e64ee1286219a862.yaml (100%) rename releasenotes/notes/{ => 1.1}/commutation-checker-utf8-47b13b78a40af196.yaml (100%) rename releasenotes/notes/{ => 1.1}/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml (100%) rename releasenotes/notes/{ => 1.1}/databin-construction-72ec041075410cb2.yaml (100%) rename releasenotes/notes/{ => 1.1}/databin-mapping-45d24d71f9bb4eda.yaml (100%) rename releasenotes/notes/{ => 1.1}/deprecate-3.8-a9db071fa3c85b1a.yaml (100%) rename releasenotes/notes/{ => 1.1}/deprecate_providerV1-ba17d7b4639d1cc5.yaml (100%) rename releasenotes/notes/{ => 1.1}/expr-bitshift-index-e9cfc6ea8729ef5e.yaml (100%) rename releasenotes/notes/{ => 1.1}/faster-lie-trotter-ba8f6dd84fe4cae4.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-custom-transpile-constraints-5defa36d540d1608.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-equivalence-setentry-5a30b0790666fcf2.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-mcx-mcp-performance-b00040804b47b200.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-missing-qubit-properties-35137aa5250d9368.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-passmanager-reuse-151877e1905d49df.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-pauli-evolve-ecr-and-name-bugs.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-pub-coerce-5d13700e15126421.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-scheduling-units-59477912b47d3dc1.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml (100%) rename releasenotes/notes/{ => 1.1}/fix_soft_compare-3f4148aab3a4606b.yaml (100%) rename releasenotes/notes/{ => 1.1}/fixes_10852-e197344c5f44b4f1.yaml (100%) rename releasenotes/notes/{ => 1.1}/fixes_11212-d6de3c007ce6d697.yaml (100%) rename releasenotes/notes/{ => 1.1}/followup_11468-61c6181e62531796.yaml (100%) rename releasenotes/notes/{ => 1.1}/histogram-style-03807965c3cc2e8a.yaml (100%) rename releasenotes/notes/{ => 1.1}/layout-compose-0b9a9a72359638d8.yaml (100%) rename releasenotes/notes/{ => 1.1}/macos-arm64-tier-1-c5030f009be6adcb.yaml (100%) rename releasenotes/notes/{ => 1.1}/nlocal-perf-3b8ebd9be1b2f4b3.yaml (100%) rename releasenotes/notes/{ => 1.1}/numpy-2.0-2f3e35bd42c48518.yaml (100%) rename releasenotes/notes/{ => 1.1}/obs-array-coerce-0d-28b192fb3d004d4a.yaml (100%) rename releasenotes/notes/{ => 1.1}/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml (100%) rename releasenotes/notes/{ => 1.1}/optimization-level2-2c8c1488173aed31.yaml (100%) rename releasenotes/notes/{ => 1.1}/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml (100%) rename releasenotes/notes/{ => 1.1}/parameter-hash-eq-645f9de55aa78d02.yaml (100%) rename releasenotes/notes/{ => 1.1}/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml (100%) rename releasenotes/notes/{ => 1.1}/pauli-apply-layout-cdcbc1bce724a150.yaml (100%) rename releasenotes/notes/{ => 1.1}/public-noncommutation-graph-dd31c931b7045a4f.yaml (100%) rename releasenotes/notes/{ => 1.1}/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml (100%) rename releasenotes/notes/{ => 1.1}/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml (100%) rename releasenotes/notes/{ => 1.1}/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml (100%) rename releasenotes/notes/{ => 1.1}/quantumcircuit-append-copy-8a9b71ad4b789490.yaml (100%) rename releasenotes/notes/{ => 1.1}/qv-perf-be76290f472e4777.yaml (100%) rename releasenotes/notes/{ => 1.1}/remove-final-reset-488247c01c4e147d.yaml (100%) rename releasenotes/notes/{ => 1.1}/removed_deprecated_0.21-741d08a01a7ed527.yaml (100%) rename releasenotes/notes/{ => 1.1}/reverse-permutation-lnn-409a07c7f6d0eed9.yaml (100%) rename releasenotes/notes/{ => 1.1}/rework-inst-durations-passes-28c78401682e22c0.yaml (100%) rename releasenotes/notes/{ => 1.1}/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml (100%) rename releasenotes/notes/{ => 1.1}/rust-two-qubit-weyl-ec551f3f9c812124.yaml (100%) rename releasenotes/notes/{ => 1.1}/sampler-pub-result-e64e7de1bae2d35e.yaml (100%) rename releasenotes/notes/{ => 1.1}/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml (100%) rename releasenotes/notes/{ => 1.1}/spo-to-matrix-26445a791e24f62a.yaml (100%) rename releasenotes/notes/{ => 1.1}/star-prerouting-0998b59880c20cef.yaml (100%) rename releasenotes/notes/{ => 1.1}/update-gate-dictionary-c0c017be67bb2f29.yaml (100%) rename releasenotes/notes/{ => 1.1}/use-target-in-transpile-7c04b14549a11f40.yaml (100%) diff --git a/docs/conf.py b/docs/conf.py index 4a79b543ed7..7081b7d4ed9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,7 @@ # The short X.Y version version = "1.1" # The full version, including alpha/beta/rc tags -release = "1.1.0" +release = "1.1.0rc1" language = "en" @@ -178,7 +178,7 @@ def linkcode_resolve(domain, info): if "qiskit" not in module_name: return None - try: + try: module = importlib.import_module(module_name) except ModuleNotFoundError: return None diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index 9084fa2f716..686366e4bb8 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -1.1.0 +1.1.0rc1 diff --git a/releasenotes/notes/abstract-commutation-analysis-3518129e91a33599.yaml b/releasenotes/notes/1.1/abstract-commutation-analysis-3518129e91a33599.yaml similarity index 100% rename from releasenotes/notes/abstract-commutation-analysis-3518129e91a33599.yaml rename to releasenotes/notes/1.1/abstract-commutation-analysis-3518129e91a33599.yaml diff --git a/releasenotes/notes/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml b/releasenotes/notes/1.1/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml similarity index 100% rename from releasenotes/notes/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml rename to releasenotes/notes/1.1/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml diff --git a/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml b/releasenotes/notes/1.1/add-backend-estimator-v2-26cf14a3612bb81a.yaml similarity index 100% rename from releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml rename to releasenotes/notes/1.1/add-backend-estimator-v2-26cf14a3612bb81a.yaml diff --git a/releasenotes/notes/add-backend-sampler-v2-5e40135781eebc7f.yaml b/releasenotes/notes/1.1/add-backend-sampler-v2-5e40135781eebc7f.yaml similarity index 100% rename from releasenotes/notes/add-backend-sampler-v2-5e40135781eebc7f.yaml rename to releasenotes/notes/1.1/add-backend-sampler-v2-5e40135781eebc7f.yaml diff --git a/releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml b/releasenotes/notes/1.1/add-bitarray-utilities-c85261138d5a1a97.yaml similarity index 100% rename from releasenotes/notes/add-bitarray-utilities-c85261138d5a1a97.yaml rename to releasenotes/notes/1.1/add-bitarray-utilities-c85261138d5a1a97.yaml diff --git a/releasenotes/notes/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml b/releasenotes/notes/1.1/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml similarity index 100% rename from releasenotes/notes/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml rename to releasenotes/notes/1.1/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml diff --git a/releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml b/releasenotes/notes/1.1/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml similarity index 100% rename from releasenotes/notes/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml rename to releasenotes/notes/1.1/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml diff --git a/releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml b/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml similarity index 100% rename from releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml rename to releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml diff --git a/releasenotes/notes/add-linear-plugin-options-b8a0ffe70dfe1676.yaml b/releasenotes/notes/1.1/add-linear-plugin-options-b8a0ffe70dfe1676.yaml similarity index 100% rename from releasenotes/notes/add-linear-plugin-options-b8a0ffe70dfe1676.yaml rename to releasenotes/notes/1.1/add-linear-plugin-options-b8a0ffe70dfe1676.yaml diff --git a/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml b/releasenotes/notes/1.1/add-run-all-plugins-option-ba8806a269e5713c.yaml similarity index 100% rename from releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml rename to releasenotes/notes/1.1/add-run-all-plugins-option-ba8806a269e5713c.yaml diff --git a/releasenotes/notes/add-scheduler-warnings-da6968a39fd8e6e7.yaml b/releasenotes/notes/1.1/add-scheduler-warnings-da6968a39fd8e6e7.yaml similarity index 100% rename from releasenotes/notes/add-scheduler-warnings-da6968a39fd8e6e7.yaml rename to releasenotes/notes/1.1/add-scheduler-warnings-da6968a39fd8e6e7.yaml diff --git a/releasenotes/notes/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml b/releasenotes/notes/1.1/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml similarity index 100% rename from releasenotes/notes/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml rename to releasenotes/notes/1.1/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml diff --git a/releasenotes/notes/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml b/releasenotes/notes/1.1/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml similarity index 100% rename from releasenotes/notes/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml rename to releasenotes/notes/1.1/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml diff --git a/releasenotes/notes/classical-store-e64ee1286219a862.yaml b/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml similarity index 100% rename from releasenotes/notes/classical-store-e64ee1286219a862.yaml rename to releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml diff --git a/releasenotes/notes/commutation-checker-utf8-47b13b78a40af196.yaml b/releasenotes/notes/1.1/commutation-checker-utf8-47b13b78a40af196.yaml similarity index 100% rename from releasenotes/notes/commutation-checker-utf8-47b13b78a40af196.yaml rename to releasenotes/notes/1.1/commutation-checker-utf8-47b13b78a40af196.yaml diff --git a/releasenotes/notes/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml b/releasenotes/notes/1.1/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml similarity index 100% rename from releasenotes/notes/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml rename to releasenotes/notes/1.1/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml diff --git a/releasenotes/notes/databin-construction-72ec041075410cb2.yaml b/releasenotes/notes/1.1/databin-construction-72ec041075410cb2.yaml similarity index 100% rename from releasenotes/notes/databin-construction-72ec041075410cb2.yaml rename to releasenotes/notes/1.1/databin-construction-72ec041075410cb2.yaml diff --git a/releasenotes/notes/databin-mapping-45d24d71f9bb4eda.yaml b/releasenotes/notes/1.1/databin-mapping-45d24d71f9bb4eda.yaml similarity index 100% rename from releasenotes/notes/databin-mapping-45d24d71f9bb4eda.yaml rename to releasenotes/notes/1.1/databin-mapping-45d24d71f9bb4eda.yaml diff --git a/releasenotes/notes/deprecate-3.8-a9db071fa3c85b1a.yaml b/releasenotes/notes/1.1/deprecate-3.8-a9db071fa3c85b1a.yaml similarity index 100% rename from releasenotes/notes/deprecate-3.8-a9db071fa3c85b1a.yaml rename to releasenotes/notes/1.1/deprecate-3.8-a9db071fa3c85b1a.yaml diff --git a/releasenotes/notes/deprecate_providerV1-ba17d7b4639d1cc5.yaml b/releasenotes/notes/1.1/deprecate_providerV1-ba17d7b4639d1cc5.yaml similarity index 100% rename from releasenotes/notes/deprecate_providerV1-ba17d7b4639d1cc5.yaml rename to releasenotes/notes/1.1/deprecate_providerV1-ba17d7b4639d1cc5.yaml diff --git a/releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml b/releasenotes/notes/1.1/expr-bitshift-index-e9cfc6ea8729ef5e.yaml similarity index 100% rename from releasenotes/notes/expr-bitshift-index-e9cfc6ea8729ef5e.yaml rename to releasenotes/notes/1.1/expr-bitshift-index-e9cfc6ea8729ef5e.yaml diff --git a/releasenotes/notes/faster-lie-trotter-ba8f6dd84fe4cae4.yaml b/releasenotes/notes/1.1/faster-lie-trotter-ba8f6dd84fe4cae4.yaml similarity index 100% rename from releasenotes/notes/faster-lie-trotter-ba8f6dd84fe4cae4.yaml rename to releasenotes/notes/1.1/faster-lie-trotter-ba8f6dd84fe4cae4.yaml diff --git a/releasenotes/notes/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml b/releasenotes/notes/1.1/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml similarity index 100% rename from releasenotes/notes/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml rename to releasenotes/notes/1.1/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml diff --git a/releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml b/releasenotes/notes/1.1/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml similarity index 100% rename from releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml rename to releasenotes/notes/1.1/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml diff --git a/releasenotes/notes/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml b/releasenotes/notes/1.1/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml similarity index 100% rename from releasenotes/notes/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml rename to releasenotes/notes/1.1/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml diff --git a/releasenotes/notes/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml b/releasenotes/notes/1.1/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml similarity index 100% rename from releasenotes/notes/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml rename to releasenotes/notes/1.1/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml diff --git a/releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml b/releasenotes/notes/1.1/fix-custom-transpile-constraints-5defa36d540d1608.yaml similarity index 100% rename from releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml rename to releasenotes/notes/1.1/fix-custom-transpile-constraints-5defa36d540d1608.yaml diff --git a/releasenotes/notes/fix-equivalence-setentry-5a30b0790666fcf2.yaml b/releasenotes/notes/1.1/fix-equivalence-setentry-5a30b0790666fcf2.yaml similarity index 100% rename from releasenotes/notes/fix-equivalence-setentry-5a30b0790666fcf2.yaml rename to releasenotes/notes/1.1/fix-equivalence-setentry-5a30b0790666fcf2.yaml diff --git a/releasenotes/notes/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml b/releasenotes/notes/1.1/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml similarity index 100% rename from releasenotes/notes/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml rename to releasenotes/notes/1.1/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml diff --git a/releasenotes/notes/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml b/releasenotes/notes/1.1/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml similarity index 100% rename from releasenotes/notes/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml rename to releasenotes/notes/1.1/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml diff --git a/releasenotes/notes/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml b/releasenotes/notes/1.1/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml similarity index 100% rename from releasenotes/notes/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml rename to releasenotes/notes/1.1/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml diff --git a/releasenotes/notes/fix-mcx-mcp-performance-b00040804b47b200.yaml b/releasenotes/notes/1.1/fix-mcx-mcp-performance-b00040804b47b200.yaml similarity index 100% rename from releasenotes/notes/fix-mcx-mcp-performance-b00040804b47b200.yaml rename to releasenotes/notes/1.1/fix-mcx-mcp-performance-b00040804b47b200.yaml diff --git a/releasenotes/notes/fix-missing-qubit-properties-35137aa5250d9368.yaml b/releasenotes/notes/1.1/fix-missing-qubit-properties-35137aa5250d9368.yaml similarity index 100% rename from releasenotes/notes/fix-missing-qubit-properties-35137aa5250d9368.yaml rename to releasenotes/notes/1.1/fix-missing-qubit-properties-35137aa5250d9368.yaml diff --git a/releasenotes/notes/fix-passmanager-reuse-151877e1905d49df.yaml b/releasenotes/notes/1.1/fix-passmanager-reuse-151877e1905d49df.yaml similarity index 100% rename from releasenotes/notes/fix-passmanager-reuse-151877e1905d49df.yaml rename to releasenotes/notes/1.1/fix-passmanager-reuse-151877e1905d49df.yaml diff --git a/releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml b/releasenotes/notes/1.1/fix-pauli-evolve-ecr-and-name-bugs.yaml similarity index 100% rename from releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml rename to releasenotes/notes/1.1/fix-pauli-evolve-ecr-and-name-bugs.yaml diff --git a/releasenotes/notes/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml b/releasenotes/notes/1.1/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml similarity index 100% rename from releasenotes/notes/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml rename to releasenotes/notes/1.1/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml diff --git a/releasenotes/notes/fix-pub-coerce-5d13700e15126421.yaml b/releasenotes/notes/1.1/fix-pub-coerce-5d13700e15126421.yaml similarity index 100% rename from releasenotes/notes/fix-pub-coerce-5d13700e15126421.yaml rename to releasenotes/notes/1.1/fix-pub-coerce-5d13700e15126421.yaml diff --git a/releasenotes/notes/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml b/releasenotes/notes/1.1/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml similarity index 100% rename from releasenotes/notes/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml rename to releasenotes/notes/1.1/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml diff --git a/releasenotes/notes/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml b/releasenotes/notes/1.1/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml similarity index 100% rename from releasenotes/notes/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml rename to releasenotes/notes/1.1/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml diff --git a/releasenotes/notes/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml b/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml similarity index 100% rename from releasenotes/notes/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml rename to releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml diff --git a/releasenotes/notes/fix-scheduling-units-59477912b47d3dc1.yaml b/releasenotes/notes/1.1/fix-scheduling-units-59477912b47d3dc1.yaml similarity index 100% rename from releasenotes/notes/fix-scheduling-units-59477912b47d3dc1.yaml rename to releasenotes/notes/1.1/fix-scheduling-units-59477912b47d3dc1.yaml diff --git a/releasenotes/notes/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml b/releasenotes/notes/1.1/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml similarity index 100% rename from releasenotes/notes/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml rename to releasenotes/notes/1.1/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml diff --git a/releasenotes/notes/fix_soft_compare-3f4148aab3a4606b.yaml b/releasenotes/notes/1.1/fix_soft_compare-3f4148aab3a4606b.yaml similarity index 100% rename from releasenotes/notes/fix_soft_compare-3f4148aab3a4606b.yaml rename to releasenotes/notes/1.1/fix_soft_compare-3f4148aab3a4606b.yaml diff --git a/releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml b/releasenotes/notes/1.1/fixes_10852-e197344c5f44b4f1.yaml similarity index 100% rename from releasenotes/notes/fixes_10852-e197344c5f44b4f1.yaml rename to releasenotes/notes/1.1/fixes_10852-e197344c5f44b4f1.yaml diff --git a/releasenotes/notes/fixes_11212-d6de3c007ce6d697.yaml b/releasenotes/notes/1.1/fixes_11212-d6de3c007ce6d697.yaml similarity index 100% rename from releasenotes/notes/fixes_11212-d6de3c007ce6d697.yaml rename to releasenotes/notes/1.1/fixes_11212-d6de3c007ce6d697.yaml diff --git a/releasenotes/notes/followup_11468-61c6181e62531796.yaml b/releasenotes/notes/1.1/followup_11468-61c6181e62531796.yaml similarity index 100% rename from releasenotes/notes/followup_11468-61c6181e62531796.yaml rename to releasenotes/notes/1.1/followup_11468-61c6181e62531796.yaml diff --git a/releasenotes/notes/histogram-style-03807965c3cc2e8a.yaml b/releasenotes/notes/1.1/histogram-style-03807965c3cc2e8a.yaml similarity index 100% rename from releasenotes/notes/histogram-style-03807965c3cc2e8a.yaml rename to releasenotes/notes/1.1/histogram-style-03807965c3cc2e8a.yaml diff --git a/releasenotes/notes/layout-compose-0b9a9a72359638d8.yaml b/releasenotes/notes/1.1/layout-compose-0b9a9a72359638d8.yaml similarity index 100% rename from releasenotes/notes/layout-compose-0b9a9a72359638d8.yaml rename to releasenotes/notes/1.1/layout-compose-0b9a9a72359638d8.yaml diff --git a/releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml b/releasenotes/notes/1.1/macos-arm64-tier-1-c5030f009be6adcb.yaml similarity index 100% rename from releasenotes/notes/macos-arm64-tier-1-c5030f009be6adcb.yaml rename to releasenotes/notes/1.1/macos-arm64-tier-1-c5030f009be6adcb.yaml diff --git a/releasenotes/notes/nlocal-perf-3b8ebd9be1b2f4b3.yaml b/releasenotes/notes/1.1/nlocal-perf-3b8ebd9be1b2f4b3.yaml similarity index 100% rename from releasenotes/notes/nlocal-perf-3b8ebd9be1b2f4b3.yaml rename to releasenotes/notes/1.1/nlocal-perf-3b8ebd9be1b2f4b3.yaml diff --git a/releasenotes/notes/numpy-2.0-2f3e35bd42c48518.yaml b/releasenotes/notes/1.1/numpy-2.0-2f3e35bd42c48518.yaml similarity index 100% rename from releasenotes/notes/numpy-2.0-2f3e35bd42c48518.yaml rename to releasenotes/notes/1.1/numpy-2.0-2f3e35bd42c48518.yaml diff --git a/releasenotes/notes/obs-array-coerce-0d-28b192fb3d004d4a.yaml b/releasenotes/notes/1.1/obs-array-coerce-0d-28b192fb3d004d4a.yaml similarity index 100% rename from releasenotes/notes/obs-array-coerce-0d-28b192fb3d004d4a.yaml rename to releasenotes/notes/1.1/obs-array-coerce-0d-28b192fb3d004d4a.yaml diff --git a/releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml b/releasenotes/notes/1.1/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml similarity index 100% rename from releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml rename to releasenotes/notes/1.1/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml diff --git a/releasenotes/notes/optimization-level2-2c8c1488173aed31.yaml b/releasenotes/notes/1.1/optimization-level2-2c8c1488173aed31.yaml similarity index 100% rename from releasenotes/notes/optimization-level2-2c8c1488173aed31.yaml rename to releasenotes/notes/1.1/optimization-level2-2c8c1488173aed31.yaml diff --git a/releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml b/releasenotes/notes/1.1/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml similarity index 100% rename from releasenotes/notes/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml rename to releasenotes/notes/1.1/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml diff --git a/releasenotes/notes/parameter-hash-eq-645f9de55aa78d02.yaml b/releasenotes/notes/1.1/parameter-hash-eq-645f9de55aa78d02.yaml similarity index 100% rename from releasenotes/notes/parameter-hash-eq-645f9de55aa78d02.yaml rename to releasenotes/notes/1.1/parameter-hash-eq-645f9de55aa78d02.yaml diff --git a/releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml similarity index 100% rename from releasenotes/notes/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml rename to releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml diff --git a/releasenotes/notes/pauli-apply-layout-cdcbc1bce724a150.yaml b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml similarity index 100% rename from releasenotes/notes/pauli-apply-layout-cdcbc1bce724a150.yaml rename to releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml diff --git a/releasenotes/notes/public-noncommutation-graph-dd31c931b7045a4f.yaml b/releasenotes/notes/1.1/public-noncommutation-graph-dd31c931b7045a4f.yaml similarity index 100% rename from releasenotes/notes/public-noncommutation-graph-dd31c931b7045a4f.yaml rename to releasenotes/notes/1.1/public-noncommutation-graph-dd31c931b7045a4f.yaml diff --git a/releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml b/releasenotes/notes/1.1/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml similarity index 100% rename from releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml rename to releasenotes/notes/1.1/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml diff --git a/releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml b/releasenotes/notes/1.1/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml similarity index 100% rename from releasenotes/notes/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml rename to releasenotes/notes/1.1/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml diff --git a/releasenotes/notes/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml b/releasenotes/notes/1.1/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml similarity index 100% rename from releasenotes/notes/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml rename to releasenotes/notes/1.1/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml diff --git a/releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml b/releasenotes/notes/1.1/quantumcircuit-append-copy-8a9b71ad4b789490.yaml similarity index 100% rename from releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml rename to releasenotes/notes/1.1/quantumcircuit-append-copy-8a9b71ad4b789490.yaml diff --git a/releasenotes/notes/qv-perf-be76290f472e4777.yaml b/releasenotes/notes/1.1/qv-perf-be76290f472e4777.yaml similarity index 100% rename from releasenotes/notes/qv-perf-be76290f472e4777.yaml rename to releasenotes/notes/1.1/qv-perf-be76290f472e4777.yaml diff --git a/releasenotes/notes/remove-final-reset-488247c01c4e147d.yaml b/releasenotes/notes/1.1/remove-final-reset-488247c01c4e147d.yaml similarity index 100% rename from releasenotes/notes/remove-final-reset-488247c01c4e147d.yaml rename to releasenotes/notes/1.1/remove-final-reset-488247c01c4e147d.yaml diff --git a/releasenotes/notes/removed_deprecated_0.21-741d08a01a7ed527.yaml b/releasenotes/notes/1.1/removed_deprecated_0.21-741d08a01a7ed527.yaml similarity index 100% rename from releasenotes/notes/removed_deprecated_0.21-741d08a01a7ed527.yaml rename to releasenotes/notes/1.1/removed_deprecated_0.21-741d08a01a7ed527.yaml diff --git a/releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml b/releasenotes/notes/1.1/reverse-permutation-lnn-409a07c7f6d0eed9.yaml similarity index 100% rename from releasenotes/notes/reverse-permutation-lnn-409a07c7f6d0eed9.yaml rename to releasenotes/notes/1.1/reverse-permutation-lnn-409a07c7f6d0eed9.yaml diff --git a/releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml b/releasenotes/notes/1.1/rework-inst-durations-passes-28c78401682e22c0.yaml similarity index 100% rename from releasenotes/notes/rework-inst-durations-passes-28c78401682e22c0.yaml rename to releasenotes/notes/1.1/rework-inst-durations-passes-28c78401682e22c0.yaml diff --git a/releasenotes/notes/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml b/releasenotes/notes/1.1/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml similarity index 100% rename from releasenotes/notes/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml rename to releasenotes/notes/1.1/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml diff --git a/releasenotes/notes/rust-two-qubit-weyl-ec551f3f9c812124.yaml b/releasenotes/notes/1.1/rust-two-qubit-weyl-ec551f3f9c812124.yaml similarity index 100% rename from releasenotes/notes/rust-two-qubit-weyl-ec551f3f9c812124.yaml rename to releasenotes/notes/1.1/rust-two-qubit-weyl-ec551f3f9c812124.yaml diff --git a/releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml b/releasenotes/notes/1.1/sampler-pub-result-e64e7de1bae2d35e.yaml similarity index 100% rename from releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml rename to releasenotes/notes/1.1/sampler-pub-result-e64e7de1bae2d35e.yaml diff --git a/releasenotes/notes/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml b/releasenotes/notes/1.1/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml similarity index 100% rename from releasenotes/notes/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml rename to releasenotes/notes/1.1/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml diff --git a/releasenotes/notes/spo-to-matrix-26445a791e24f62a.yaml b/releasenotes/notes/1.1/spo-to-matrix-26445a791e24f62a.yaml similarity index 100% rename from releasenotes/notes/spo-to-matrix-26445a791e24f62a.yaml rename to releasenotes/notes/1.1/spo-to-matrix-26445a791e24f62a.yaml diff --git a/releasenotes/notes/star-prerouting-0998b59880c20cef.yaml b/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml similarity index 100% rename from releasenotes/notes/star-prerouting-0998b59880c20cef.yaml rename to releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml diff --git a/releasenotes/notes/update-gate-dictionary-c0c017be67bb2f29.yaml b/releasenotes/notes/1.1/update-gate-dictionary-c0c017be67bb2f29.yaml similarity index 100% rename from releasenotes/notes/update-gate-dictionary-c0c017be67bb2f29.yaml rename to releasenotes/notes/1.1/update-gate-dictionary-c0c017be67bb2f29.yaml diff --git a/releasenotes/notes/use-target-in-transpile-7c04b14549a11f40.yaml b/releasenotes/notes/1.1/use-target-in-transpile-7c04b14549a11f40.yaml similarity index 100% rename from releasenotes/notes/use-target-in-transpile-7c04b14549a11f40.yaml rename to releasenotes/notes/1.1/use-target-in-transpile-7c04b14549a11f40.yaml From 27bd5e79f56a396933e030d178cebcf972320c34 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Sun, 5 May 2024 11:23:31 -0400 Subject: [PATCH 037/159] Link to `qiskit.synthesis.unitary.aqc` on API index page (#12331) * Link to `qiskit.synthesis.unitary.aqc` on API index page * Add to index page --- docs/apidoc/index.rst | 1 + docs/apidoc/qiskit.synthesis.unitary.aqc.rst | 6 ++++++ qiskit/synthesis/__init__.py | 7 +------ 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 docs/apidoc/qiskit.synthesis.unitary.aqc.rst diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 89a2a6bb9a6..3a6c1b04cfd 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -25,6 +25,7 @@ API Reference pulse scheduler synthesis + qiskit.synthesis.unitary.aqc primitives qasm2 qasm3 diff --git a/docs/apidoc/qiskit.synthesis.unitary.aqc.rst b/docs/apidoc/qiskit.synthesis.unitary.aqc.rst new file mode 100644 index 00000000000..5f3219a40db --- /dev/null +++ b/docs/apidoc/qiskit.synthesis.unitary.aqc.rst @@ -0,0 +1,6 @@ +.. _qiskit-synthesis_unitary_aqc: + +.. automodule:: qiskit.synthesis.unitary.aqc + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 49e7885d509..b46c8eac545 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -99,12 +99,7 @@ .. autofunction:: qs_decomposition -The Approximate Quantum Compiler is available here: - -.. autosummary:: - :toctree: ../stubs/ - - qiskit.synthesis.unitary.aqc +The Approximate Quantum Compiler is available as the module :mod:`qiskit.synthesis.unitary.aqc`. One-Qubit Synthesis =================== From c1d728a9c8c51eaf12b4f118107ffc7cbeda956d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 6 May 2024 02:52:31 -0400 Subject: [PATCH 038/159] Bump main branch version post 1.1.0rc1 tag (#12338) Now that the first release candidate for the 1.1.0 release has been tagged, the stable branch for the 1.1 series has been created and we can start developing the 1.1.0 release on main. This commit bumps all the version strings from 1.1.0 to 1.2.0 (and the backport branch for mergify to stable/1.1) accordingly to differentiate the main branch from 1.1.*. --- .mergify.yml | 2 +- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- docs/conf.py | 4 ++-- qiskit/VERSION.txt | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 87a3438930f..10b6d202225 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,4 +6,4 @@ pull_request_rules: actions: backport: branches: - - stable/1.0 + - stable/1.1 diff --git a/Cargo.lock b/Cargo.lock index 50903b3811a..84e0e009066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "qiskit-accelerate" -version = "1.1.0" +version = "1.2.0" dependencies = [ "ahash 0.8.11", "approx", @@ -1122,7 +1122,7 @@ dependencies = [ [[package]] name = "qiskit-circuit" -version = "1.1.0" +version = "1.2.0" dependencies = [ "hashbrown 0.14.5", "pyo3", @@ -1130,7 +1130,7 @@ dependencies = [ [[package]] name = "qiskit-pyext" -version = "1.1.0" +version = "1.2.0" dependencies = [ "pyo3", "qiskit-accelerate", @@ -1141,7 +1141,7 @@ dependencies = [ [[package]] name = "qiskit-qasm2" -version = "1.1.0" +version = "1.2.0" dependencies = [ "hashbrown 0.14.5", "pyo3", @@ -1150,7 +1150,7 @@ dependencies = [ [[package]] name = "qiskit-qasm3" -version = "1.1.0" +version = "1.2.0" dependencies = [ "hashbrown 0.14.5", "indexmap 2.2.6", diff --git a/Cargo.toml b/Cargo.toml index 9c4af6260be..2827b2206f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "1.1.0" +version = "1.2.0" edition = "2021" rust-version = "1.70" # Keep in sync with README.md and rust-toolchain.toml. license = "Apache-2.0" diff --git a/docs/conf.py b/docs/conf.py index 7081b7d4ed9..b35f5ca64d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,9 +30,9 @@ author = "Qiskit Development Team" # The short X.Y version -version = "1.1" +version = "1.2" # The full version, including alpha/beta/rc tags -release = "1.1.0rc1" +release = "1.2.0" language = "en" diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index 686366e4bb8..26aaba0e866 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -1.1.0rc1 +1.2.0 From 87a0396095ae348cd5fb90d8cef805674edfb8fc Mon Sep 17 00:00:00 2001 From: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> Date: Mon, 6 May 2024 12:23:21 +0530 Subject: [PATCH 039/159] Fix typo under 'pull request checklist' heading (#12340) --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ea3dc9f60f..c75b5175001 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,8 +183,8 @@ please ensure that: If your pull request is adding a new class, function, or module that is intended to be user facing ensure that you've also added those to a documentation `autosummary` index to include it in the api documentation. -3. If it makes sense for your change that you have added new tests that - cover the changes. +3. If you are of the opinion that the modifications you made warrant additional tests, + feel free to include them 4. Ensure that if your change has an end user facing impact (new feature, deprecation, removal etc) that you have added a reno release note for that change and that the PR is tagged for the changelog. From 1412a5e5ab1d131fb42502338aa1ae49c64cd6c2 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Mon, 6 May 2024 02:54:40 -0400 Subject: [PATCH 040/159] Removing lint rule use-implicit-booleaness-not-comparison and updates (#12218) * removing lint rule use-implicit-booleaness-not-comparison and updates * fix merge mistake --- pyproject.toml | 1 - qiskit/pulse/instruction_schedule_map.py | 2 +- qiskit/synthesis/linear_phase/cnot_phase_synth.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97ccde21d1b..9c7094827c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,7 +231,6 @@ disable = [ "unnecessary-lambda-assignment", "unspecified-encoding", "unsupported-assignment-operation", - "use-implicit-booleaness-not-comparison", ] enable = [ diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index 3d3767509f2..decda15c994 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -250,7 +250,7 @@ def add( # validation of target qubit qubits = _to_tuple(qubits) - if qubits == (): + if not qubits: raise PulseError(f"Cannot add definition {instruction} with no target qubits.") # generate signature diff --git a/qiskit/synthesis/linear_phase/cnot_phase_synth.py b/qiskit/synthesis/linear_phase/cnot_phase_synth.py index b107241310f..25320029ef5 100644 --- a/qiskit/synthesis/linear_phase/cnot_phase_synth.py +++ b/qiskit/synthesis/linear_phase/cnot_phase_synth.py @@ -123,7 +123,7 @@ def synth_cnot_phase_aam( # Implementation of the pseudo-code (Algorithm 1) in the aforementioned paper sta.append([cnots, range_list, epsilon]) - while sta != []: + while sta: [cnots, ilist, qubit] = sta.pop() if cnots == []: continue From f39c9070692e3955ada776e1c3fc537fe00fefd1 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Mon, 6 May 2024 08:49:40 -0400 Subject: [PATCH 041/159] Improve header hierarchy for API module pages (#12325) * Improve header hierarchy for API module pages * Tweaks * Review feedback --- qiskit/assembler/__init__.py | 15 +-- qiskit/circuit/__init__.py | 4 +- qiskit/circuit/classical/types/__init__.py | 9 +- qiskit/circuit/classicalfunction/__init__.py | 1 + qiskit/circuit/library/__init__.py | 22 +--- qiskit/converters/__init__.py | 19 ++- qiskit/providers/__init__.py | 29 ++--- qiskit/providers/basic_provider/__init__.py | 25 +--- qiskit/providers/fake_provider/__init__.py | 2 +- qiskit/providers/models/__init__.py | 4 +- qiskit/qpy/__init__.py | 118 +++++++++---------- qiskit/result/__init__.py | 6 + qiskit/scheduler/__init__.py | 11 +- qiskit/scheduler/methods/__init__.py | 9 +- qiskit/transpiler/passes/__init__.py | 6 +- 15 files changed, 129 insertions(+), 151 deletions(-) diff --git a/qiskit/assembler/__init__.py b/qiskit/assembler/__init__.py index a356501a263..45798084ea6 100644 --- a/qiskit/assembler/__init__.py +++ b/qiskit/assembler/__init__.py @@ -17,23 +17,18 @@ .. currentmodule:: qiskit.assembler -Circuit Assembler -================= +Functions +========= -.. autofunction:: assemble_circuits -Schedule Assembler -================== +.. autofunction:: assemble_circuits .. autofunction:: assemble_schedules -Disassembler -============ - .. autofunction:: disassemble -RunConfig -========= +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 9fbefb4c5d9..ff36550967d 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -203,8 +203,8 @@ .. _circuit-module-api: -API overview of :mod:`qiskit.circuit` -===================================== +API overview of qiskit.circuit +============================== All objects here are described in more detail, and in their greater context in the following sections. This section provides an overview of the API elements documented here. diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 93ab90e3216..14365fd32a6 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -47,13 +47,14 @@ Working with types ================== -There are some functions on these types exposed here as well. These are mostly expected to be used -only in manipulations of the expression tree; users who are building expressions using the +There are some additional functions on these types documented in the subsequent sections. +These are mostly expected to be used only in manipulations of the expression tree; +users who are building expressions using the :ref:`user-facing construction interface ` should not need to use these. Partial ordering of types -------------------------- +========================= The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as ":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the @@ -78,7 +79,7 @@ Casting between types ---------------------- +===================== It is common to need to cast values of one type to another type. The casting rules for this are embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py index a2268acfe2d..a072d910f97 100644 --- a/qiskit/circuit/classicalfunction/__init__.py +++ b/qiskit/circuit/classicalfunction/__init__.py @@ -81,6 +81,7 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1: Decorator for a classical function that returns a `ClassicalFunction` object. +.. autofunction:: classical_function ClassicalFunction ----------------- diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 5f21967e482..a9ae005d982 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -129,35 +129,19 @@ Standard Directives =================== -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - Directives are operations to the quantum stack that are meant to be interpreted by the backend or the transpiler. In general, the transpiler or backend might optionally ignore them if there is no implementation for them. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Barrier +* :class:`qiskit.circuit.Barrier` Standard Operations =================== Operations are non-reversible changes in the quantum state of the circuit. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Measure - Reset +* :class:`qiskit.circuit.Measure` +* :class:`qiskit.circuit.Reset` Generalized Gates ================= diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index 459b739ee01..f3d3edb5b77 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -17,12 +17,27 @@ .. currentmodule:: qiskit.converters -.. autofunction:: circuit_to_dag -.. autofunction:: dag_to_circuit +QuantumCircuit -> circuit components +==================================== + .. autofunction:: circuit_to_instruction .. autofunction:: circuit_to_gate + +QuantumCircuit <-> DagCircuit +============================= + +.. autofunction:: circuit_to_dag +.. autofunction:: dag_to_circuit + +QuantumCircuit <-> DagDependency +================================ + .. autofunction:: dagdependency_to_circuit .. autofunction:: circuit_to_dagdependency + +DagCircuit <-> DagDependency +============================ + .. autofunction:: dag_to_dagdependency .. autofunction:: dagdependency_to_dag """ diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 19d300c9bbf..b0ebc942523 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -131,7 +131,6 @@ .. autoexception:: JobTimeoutError .. autoexception:: BackendConfigurationError -===================== Writing a New Backend ===================== @@ -164,7 +163,7 @@ `qiskit-aqt-provider `__ Provider -======== +-------- A provider class serves a single purpose: to get backend objects that enable executing circuits on a device or simulator. The expectation is that any @@ -195,7 +194,7 @@ def backends(self, name=None, **kwargs): method matches the required interface. The rest is up to the specific provider on how to implement. Backend -======= +------- The backend classes are the core to the provider. These classes are what provide the interface between Qiskit and the hardware or simulator that will @@ -276,8 +275,8 @@ def run(circuits, **kwargs): return MyJob(self. job_handle, job_json, circuit) -Transpiler Interface --------------------- +Backend's Transpiler Interface +------------------------------ The key piece of the :class:`~qiskit.providers.Backend` object is how it describes itself to the compiler. This is handled with the :class:`~qiskit.transpiler.Target` class which defines @@ -453,8 +452,8 @@ def get_translation_stage_plugin(self): efficient output on ``Mybackend`` the transpiler will be able to perform these custom steps without any manual user input. -Run Method ----------- +Backend.run Method +-------------------- Of key importance is the :meth:`~qiskit.providers.BackendV2.run` method, which is used to actually submit circuits to a device or simulator. The run method @@ -484,8 +483,8 @@ def run(self, circuits. **kwargs): job_handle = submit_to_backend(job_jsonb) return MyJob(self. job_handle, job_json, circuit) -Options -------- +Backend Options +--------------- There are often several options for a backend that control how a circuit is run. The typical example of this is something like the number of ``shots`` which is @@ -515,7 +514,7 @@ def _default_options(cls): Job -=== +--- The output from the :obj:`~qiskit.providers.BackendV2.run` method is a :class:`~qiskit.providers.JobV1` object. Each provider is expected to implement a custom job subclass that @@ -612,7 +611,7 @@ def status(self): return JobStatus.DONE Primitives -========== +---------- While not directly part of the provider interface, the :mod:`qiskit.primitives` module is tightly coupled with providers. Specifically the primitive @@ -640,12 +639,8 @@ def status(self): :class:`~.Estimator`, :class:`~.BackendSampler`, and :class:`~.BackendEstimator` can serve as references/models on how to implement these as well. -====================================== -Migrating between Backend API Versions -====================================== - -BackendV1 -> BackendV2 -====================== +Migrating from BackendV1 to BackendV2 +===================================== The :obj:`~BackendV2` class re-defined user access for most properties of a backend to make them work with native Qiskit data structures and have flatter diff --git a/qiskit/providers/basic_provider/__init__.py b/qiskit/providers/basic_provider/__init__.py index 48427c73fca..4fc0f06d76a 100644 --- a/qiskit/providers/basic_provider/__init__.py +++ b/qiskit/providers/basic_provider/__init__.py @@ -27,36 +27,15 @@ backend = BasicProvider().get_backend('basic_simulator') -Simulators -========== +Classes +======= .. autosummary:: :toctree: ../stubs/ BasicSimulator - -Provider -======== - -.. autosummary:: - :toctree: ../stubs/ - BasicProvider - -Job Class -========= - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderJob - -Exceptions -========== - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderError """ diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 00dadd2ad25..9526793f0e1 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -24,7 +24,7 @@ useful for testing the transpiler and other backend-facing functionality. Example Usage -============= +------------- Here is an example of using a simulated backend for transpilation and running. diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index a69038eb78c..bf90a9d16c0 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -19,8 +19,8 @@ Qiskit schema-conformant objects used by the backends and providers. -Backend Objects -=============== +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index d7275bcd62f..4e7769106fc 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -########################################################### +===================================== QPY serialization (:mod:`qiskit.qpy`) -########################################################### +===================================== .. currentmodule:: qiskit.qpy @@ -32,9 +32,8 @@ version (it is also `potentially insecure `__). -********* -Using QPY -********* +Basic Usage +=========== Using QPY is defined to be straightforward and mirror the user API of the serializers in Python's standard library, ``pickle`` and ``json``. There are @@ -248,9 +247,8 @@ .. _qpy_format: -********** QPY Format -********** +========== The QPY serialization format is a portable cross-platform binary serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic @@ -303,14 +301,14 @@ .. _qpy_version_12: Version 12 -========== +---------- Version 12 adds support for: * circuits containing memory-owning :class:`.expr.Var` variables. Changes to HEADER ------------------ +~~~~~~~~~~~~~~~~~ The HEADER struct for an individual circuit has added three ``uint32_t`` counts of the input, captured and locally declared variables in the circuit. The new form looks like: @@ -336,7 +334,7 @@ EXPR_VAR_DECLARATION --------------------- +~~~~~~~~~~~~~~~~~~~~ An ``EXPR_VAR_DECLARATION`` defines an :class:`.expr.Var` instance that is standalone; that is, it represents a self-owned memory location rather than wrapping a :class:`.Clbit` or @@ -367,7 +365,7 @@ Changes to EXPR_VAR -------------------- +~~~~~~~~~~~~~~~~~~~ The EXPR_VAR variable has gained a new type code and payload, in addition to the pre-existing ones: @@ -400,7 +398,7 @@ .. _qpy_version_11: Version 11 -========== +---------- Version 11 is identical to Version 10 except for the following. First, the names in the CUSTOM_INSTRUCTION blocks @@ -418,7 +416,7 @@ .. _modifier_qpy: MODIFIER --------- +~~~~~~~~ This represents :class:`~qiskit.circuit.annotated_operation.Modifier` @@ -441,7 +439,7 @@ .. _qpy_version_10: Version 10 -========== +---------- Version 10 adds support for: @@ -454,7 +452,7 @@ encoding and ``e`` refers to symengine encoding. Changes to FILE_HEADER ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ The contents of FILE_HEADER after V10 are defined as a C struct as: @@ -470,7 +468,7 @@ } FILE_HEADER_V10; Changes to LAYOUT ------------------ +~~~~~~~~~~~~~~~~~ The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field. With version 10 the ``LAYOUT`` struct is now: @@ -493,14 +491,14 @@ .. _qpy_version_9: Version 9 -========= +--------- Version 9 adds support for classical :class:`~.expr.Expr` nodes and their associated :class:`~.types.Type`\\ s. EXPRESSION ----------- +~~~~~~~~~~ An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is represented by (in order in the byte stream): @@ -532,7 +530,7 @@ EXPR_TYPE ---------- +~~~~~~~~~ A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, followed by a payload that varies depending on the type. The defined codes are: @@ -547,7 +545,7 @@ EXPR_VAR --------- +~~~~~~~~ This represents a runtime variable of a :class:`~.expr.Var` node. These are a type code, followed by a type-code-specific payload: @@ -564,7 +562,7 @@ EXPR_VALUE ----------- +~~~~~~~~~~ This represents a literal object in the classical type system, such as an integer. Currently there are very few such literals. These are encoded as a type code, followed by a type-code-specific @@ -582,7 +580,7 @@ Changes to INSTRUCTION ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ To support the use of :class:`~.expr.Expr` nodes in the fields :attr:`.IfElseOp.condition`, :attr:`.WhileLoopOp.condition` and :attr:`.SwitchCaseOp.target`, the INSTRUCTION struct is changed @@ -629,7 +627,7 @@ Changes to INSTRUCTION_PARAM ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A new type code ``x`` is added that defines an EXPRESSION parameter. @@ -637,7 +635,7 @@ .. _qpy_version_8: Version 8 -========= +--------- Version 8 adds support for handling a :class:`~.TranspileLayout` stored in the :attr:`.QuantumCircuit.layout` attribute. In version 8 immediately following the @@ -646,7 +644,7 @@ :class:`~.TranspileLayout` class. LAYOUT ------- +~~~~~~ .. code-block:: c @@ -668,7 +666,7 @@ :attr:`.TranspileLayout.initial_layout` attribute. INITIAL_LAYOUT_BIT ------------------- +~~~~~~~~~~~~~~~~~~ .. code-block:: c @@ -694,7 +692,7 @@ .. _qpy_version_7: Version 7 -========= +--------- Version 7 adds support for :class:`.~Reference` instruction and serialization of a :class:`.~ScheduleBlock` program while keeping its reference to subroutines:: @@ -740,7 +738,7 @@ .. _qpy_version_6: Version 6 -========= +--------- Version 6 adds support for :class:`.~ScalableSymbolicPulse`. These objects are saved and read like `SymbolicPulse` objects, and the class name is added to the data to correctly handle @@ -767,7 +765,7 @@ .. _qpy_version_5: Version 5 -========= +--------- Version 5 changes from :ref:`qpy_version_4` by adding support for :class:`.~ScheduleBlock` and changing two payloads the INSTRUCTION metadata payload and the CUSTOM_INSTRUCTION block. @@ -802,7 +800,7 @@ .. _qpy_schedule_block: SCHEDULE_BLOCK --------------- +~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` is first supported in QPY Version 5. This allows users to save pulse programs in the QPY binary format as follows: @@ -827,7 +825,7 @@ .. _qpy_schedule_block_header: SCHEDULE_BLOCK_HEADER ---------------------- +~~~~~~~~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` block starts with the following header: @@ -846,7 +844,7 @@ .. _qpy_schedule_alignments: SCHEDULE_BLOCK_ALIGNMENTS -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ Then, alignment context of the schedule block starts with ``char`` representing the supported context type followed by the :ref:`qpy_sequence` block representing @@ -864,7 +862,7 @@ .. _qpy_schedule_instructions: SCHEDULE_BLOCK_INSTRUCTIONS ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ This alignment block is further followed by ``num_element`` length of block elements which may consist of nested schedule blocks and schedule instructions. @@ -889,7 +887,7 @@ .. _qpy_schedule_operands: SCHEDULE_BLOCK_OPERANDS ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The operands of these instances can be serialized through the standard QPY value serialization mechanism, however there are special object types that only appear in the schedule operands. @@ -906,7 +904,7 @@ .. _qpy_schedule_channel: CHANNEL -------- +~~~~~~~ Channel block starts with channel subtype ``char`` that maps an object data to :class:`~qiskit.pulse.channels.Channel` subclass. Mapping is defined as follows: @@ -923,7 +921,7 @@ .. _qpy_schedule_waveform: Waveform --------- +~~~~~~~~ Waveform block starts with WAVEFORM header: @@ -945,7 +943,7 @@ .. _qpy_schedule_symbolic_pulse: SymbolicPulse -------------- +~~~~~~~~~~~~~ SymbolicPulse block starts with SYMBOLIC_PULSE header: @@ -979,7 +977,7 @@ .. _qpy_mapping: MAPPING -------- +~~~~~~~ The MAPPING is a representation for arbitrary mapping object. This is a fixed length :ref:`qpy_sequence` of key-value pair represented by the MAP_ITEM payload. @@ -1001,7 +999,7 @@ .. _qpy_circuit_calibrations: CIRCUIT_CALIBRATIONS --------------------- +~~~~~~~~~~~~~~~~~~~~ The CIRCUIT_CALIBRATIONS block is a dictionary to define pulse calibrations of the custom instruction set. This block starts with the following CALIBRATION header: @@ -1036,7 +1034,7 @@ .. _qpy_instruction_v5: INSTRUCTION ------------ +~~~~~~~~~~~ The INSTRUCTION block was modified to add two new fields ``num_ctrl_qubits`` and ``ctrl_state`` which are used to model the :attr:`.ControlledGate.num_ctrl_qubits` and @@ -1062,7 +1060,7 @@ :ref:`qpy_instructions` for the details of the full payload. CUSTOM_INSTRUCTION ------------------- +~~~~~~~~~~~~~~~~~~ The CUSTOM_INSTRUCTION block in QPY version 5 adds a new field ``base_gate_size`` which is used to define the size of the @@ -1105,7 +1103,7 @@ .. _qpy_version_4: Version 4 -========= +--------- Version 4 is identical to :ref:`qpy_version_3` except that it adds 2 new type strings to the INSTRUCTION_PARAM struct, ``z`` to represent ``None`` (which is encoded as @@ -1135,7 +1133,7 @@ .. _qpy_range_pack: RANGE ------ +~~~~~ A RANGE is a representation of a ``range`` object. It is defined as: @@ -1150,7 +1148,7 @@ .. _qpy_sequence: SEQUENCE --------- +~~~~~~~~ A SEQUENCE is a representation of an arbitrary sequence object. As sequence are just fixed length containers of arbitrary python objects their QPY can't fully represent any sequence, @@ -1172,7 +1170,7 @@ .. _qpy_version_3: Version 3 -========= +--------- Version 3 of the QPY format is identical to :ref:`qpy_version_2` except that it defines a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1187,7 +1185,7 @@ .. _pauli_evo_qpy: PAULI_EVOLUTION ---------------- +~~~~~~~~~~~~~~~ This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1215,7 +1213,7 @@ .. _qpy_pauli_sum_op: SPARSE_PAULI_OP_LIST_ELEM -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ This represents an instance of :class:`.SparsePauliOp`. @@ -1239,7 +1237,7 @@ .. _qpy_param_vector: PARAMETER_VECTOR_ELEMENT ------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~ A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are @@ -1261,7 +1259,7 @@ PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ Additionally, since QPY format version v3 distinguishes between a :class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement` @@ -1315,14 +1313,14 @@ .. _qpy_version_2: Version 2 -========= +--------- Version 2 of the QPY format is identical to version 1 except for the HEADER section is slightly different. You can refer to the :ref:`qpy_version_1` section for the details on the rest of the payload format. HEADER ------- +~~~~~~ The contents of HEADER are defined as a C struct are: @@ -1352,10 +1350,10 @@ .. _qpy_version_1: Version 1 -========= +--------- HEADER ------- +~~~~~~ The contents of HEADER as defined as a C struct are: @@ -1375,7 +1373,7 @@ of the circuit. METADATA --------- +~~~~~~~~ The METADATA field is a UTF8 encoded JSON string. After reading the HEADER (which is a fixed size at the start of the QPY file) and the ``name`` string @@ -1385,7 +1383,7 @@ .. _qpy_registers: REGISTERS ---------- +~~~~~~~~~ The contents of REGISTERS is a number of REGISTER object. If num_registers is > 0 then after reading METADATA you read that number of REGISTER structs defined @@ -1435,7 +1433,7 @@ .. _qpy_custom_definition: CUSTOM_DEFINITIONS ------------------- +~~~~~~~~~~~~~~~~~~ This section specifies custom definitions for any of the instructions in the circuit. @@ -1475,7 +1473,7 @@ .. _qpy_instructions: INSTRUCTIONS ------------- +~~~~~~~~~~~~ The contents of INSTRUCTIONS is a list of INSTRUCTION metadata objects @@ -1551,7 +1549,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_struct: PARAMETER ---------- +~~~~~~~~~ A PARAMETER represents a :class:`~qiskit.circuit.Parameter` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER are defined as: @@ -1569,7 +1567,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_expr: PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR @@ -1608,7 +1606,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_complex: COMPLEX -------- +~~~~~~~ When representing a double precision complex value in QPY the following struct is used: diff --git a/qiskit/result/__init__.py b/qiskit/result/__init__.py index 08b43f70493..2eaa7803c5f 100644 --- a/qiskit/result/__init__.py +++ b/qiskit/result/__init__.py @@ -17,6 +17,9 @@ .. currentmodule:: qiskit.result +Core classes +============ + .. autosummary:: :toctree: ../stubs/ @@ -24,6 +27,9 @@ ResultError Counts +Marginalization +=============== + .. autofunction:: marginal_counts .. autofunction:: marginal_distribution .. autofunction:: marginal_memory diff --git a/qiskit/scheduler/__init__.py b/qiskit/scheduler/__init__.py index b33ececf5d6..7062e01a941 100644 --- a/qiskit/scheduler/__init__.py +++ b/qiskit/scheduler/__init__.py @@ -19,13 +19,22 @@ A circuit scheduler compiles a circuit program to a pulse program. +Core API +======== + .. autoclass:: ScheduleConfig .. currentmodule:: qiskit.scheduler.schedule_circuit .. autofunction:: schedule_circuit .. currentmodule:: qiskit.scheduler -.. automodule:: qiskit.scheduler.methods +Pulse scheduling methods +======================== + +.. currentmodule:: qiskit.scheduler.methods +.. autofunction:: as_soon_as_possible +.. autofunction:: as_late_as_possible +.. currentmodule:: qiskit.scheduler """ from qiskit.scheduler import schedule_circuit from qiskit.scheduler.config import ScheduleConfig diff --git a/qiskit/scheduler/methods/__init__.py b/qiskit/scheduler/methods/__init__.py index 1fe4b301b7a..6df887d5499 100644 --- a/qiskit/scheduler/methods/__init__.py +++ b/qiskit/scheduler/methods/__init__.py @@ -10,13 +10,6 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -.. currentmodule:: qiskit.scheduler.methods - -Pulse scheduling methods. - -.. autofunction:: as_soon_as_possible -.. autofunction:: as_late_as_possible -""" +"""Scheduling methods.""" from qiskit.scheduler.methods.basic import as_soon_as_possible, as_late_as_possible diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 54599e00b9a..400d9830495 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -154,8 +154,10 @@ HLSConfig SolovayKitaev -Post Layout (Post transpile qubit selection) -============================================ +Post Layout +=========== + +These are post qubit selection. .. autosummary:: :toctree: ../stubs/ From 2c418aa9b359645642230c1cecb5122e9d01de56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 13:27:12 +0000 Subject: [PATCH 042/159] Bump num-traits from 0.2.18 to 0.2.19 (#12349) Bumps [num-traits](https://github.com/rust-num/num-traits) from 0.2.18 to 0.2.19. - [Changelog](https://github.com/rust-num/num-traits/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-traits/compare/num-traits-0.2.18...num-traits-0.2.19) --- updated-dependencies: - dependency-name: num-traits 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 84e0e009066..c8b22ed7917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", From c062dd6cf22558f630903d530728f2e1b60dacc3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 6 May 2024 15:08:19 -0400 Subject: [PATCH 043/159] Fix Rustfmt and clippy with latest rust stable (#12339) * Fix Rustfmt and clippy with latest rust stable Running `cargo fmt` with a newer version of rust than what we use in CI (1.77.2 and 1.78.0 locally for me) is triggering the formatting updates in this commit. To ensure developers don't have to worry about commiting an accidental formatting change in their commits this commit proactively makes the change. Similarly the recent Rust 1.78 release included new clippy rules which are flagging some small issues that our MSRV of clippy doesn't have. This commit also fixes these as the suggestions are good and are compatible with our MSRV of 1.70. * Rename deprecated config file name * Remove dead code --- .cargo/{config => config.toml} | 0 crates/accelerate/src/sparse_pauli_op.rs | 4 +++- crates/circuit/src/circuit_data.rs | 2 +- crates/qasm3/src/circuit.rs | 10 ---------- 4 files changed, 4 insertions(+), 12 deletions(-) rename .cargo/{config => config.toml} (100%) diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 5d6a82df794..808269d8ab9 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -141,7 +141,9 @@ impl ZXPaulis { phases: &Bound>, coeffs: &Bound>, ) -> PyResult { - let &[num_ops, num_qubits] = x.shape() else { unreachable!("PyArray2 must be 2D") }; + let &[num_ops, num_qubits] = x.shape() else { + unreachable!("PyArray2 must be 2D") + }; if z.shape() != [num_ops, num_qubits] { return Err(PyValueError::new_err(format!( "'x' and 'z' have different shapes: {:?} and {:?}", diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 590fc07e8f8..944565cf36d 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -324,7 +324,7 @@ impl CircuitData { 0, )?; res.intern_context = self.intern_context.clone(); - res.data = self.data.clone(); + res.data.clone_from(&self.data); Ok(res) } diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 747980819a0..330805fa2f8 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -16,7 +16,6 @@ use pyo3::types::{PyList, PyString, PyTuple, PyType}; use crate::error::QASM3ImporterError; pub trait PyRegister { - fn bit(&self, py: Python, index: usize) -> PyResult>; // This really should be // fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; // or at a minimum @@ -39,15 +38,6 @@ macro_rules! register_type { } impl PyRegister for $name { - /// Get an individual bit from the register. - fn bit(&self, py: Python, index: usize) -> PyResult> { - // Unfortunately, `PyList::get_item_unchecked` isn't usable with the stable ABI. - self.items - .bind(py) - .get_item(index) - .map(|item| item.into_py(py)) - } - fn bit_list<'a>(&'a self, py: Python<'a>) -> &Bound<'a, PyList> { self.items.bind(py) } From 6ff0eb54f88cdbe98275872b8ec9d65f76948bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 7 May 2024 11:49:31 +0200 Subject: [PATCH 044/159] Add standalone test file for Clifford synthesis functions (#12347) * Convert synthesis imports to runtime imports to avoid cyclic import errors * Set copy=False in append * Move clifford synthesis tests to separate file in test/synthesis. * Add random_clifford_circuit to qiskit.circuit.random * Remove pylint disable * Fix reno --- .../generalized_gates/linear_function.py | 5 +- .../circuit/library/generalized_gates/uc.py | 2 +- .../library/generalized_gates/unitary.py | 18 +- .../n_local/evolved_operator_ansatz.py | 5 +- qiskit/circuit/library/pauli_evolution.py | 10 +- qiskit/circuit/random/__init__.py | 2 +- qiskit/circuit/random/utils.py | 71 ++++++- .../quantum_info/operators/dihedral/random.py | 6 +- .../clifford/clifford_decompose_bm.py | 6 +- .../clifford/clifford_decompose_layers.py | 18 +- ...random-clifford-util-5358041208729988.yaml | 14 ++ .../operators/symplectic/test_clifford.py | 187 +----------------- .../synthesis/test_clifford_sythesis.py | 118 +++++++++++ 13 files changed, 248 insertions(+), 214 deletions(-) create mode 100644 releasenotes/notes/add-random-clifford-util-5358041208729988.yaml create mode 100644 test/python/synthesis/test_clifford_sythesis.py diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index 68deaddd732..519a306c357 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2021. +# (C) Copyright IBM 2017, 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 @@ -16,7 +16,6 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError -from qiskit.synthesis.linear import check_invertible_binary_matrix from qiskit.circuit.library.generalized_gates.permutation import PermutationGate # pylint: disable=cyclic-import @@ -115,6 +114,8 @@ def __init__( # Optionally, check that the matrix is invertible if validate_input: + from qiskit.synthesis.linear import check_invertible_binary_matrix + if not check_invertible_binary_matrix(linear): raise CircuitError( "A linear function must be represented by an invertible matrix." diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index f54567123e0..6e6a1db95ca 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 1fd36e52e0c..6a6623ffce5 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 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 @@ -30,14 +30,8 @@ from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.predicates import is_unitary_matrix -# pylint: disable=cyclic-import -from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer -from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose - from .isometry import Isometry -_DECOMPOSER1Q = OneQubitEulerDecomposer("U") - if typing.TYPE_CHECKING: from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -143,13 +137,21 @@ def transpose(self): def _define(self): """Calculate a subcircuit that implements this unitary.""" if self.num_qubits == 1: + from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer + q = QuantumRegister(1, "q") qc = QuantumCircuit(q, name=self.name) - theta, phi, lam, global_phase = _DECOMPOSER1Q.angles_and_phase(self.to_matrix()) + theta, phi, lam, global_phase = OneQubitEulerDecomposer("U").angles_and_phase( + self.to_matrix() + ) qc._append(UGate(theta, phi, lam), [q[0]], []) qc.global_phase = global_phase self.definition = qc elif self.num_qubits == 2: + from qiskit.synthesis.two_qubit.two_qubit_decompose import ( # pylint: disable=cyclic-import + two_qubit_cnot_decompose, + ) + self.definition = two_qubit_cnot_decompose(self.to_matrix()) else: from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py index a50b48ce488..4bc6bcc58a1 100644 --- a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -22,7 +22,6 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info import Operator, Pauli, SparsePauliOp -from qiskit.synthesis.evolution import LieTrotter from .n_local import NLocal @@ -185,6 +184,8 @@ def _evolve_operator(self, operator, time): gate = HamiltonianGate(operator, time) # otherwise, use the PauliEvolutionGate else: + from qiskit.synthesis.evolution import LieTrotter + evolution = LieTrotter() if self._evolution is None else self._evolution gate = PauliEvolutionGate(operator, time, synthesis=evolution) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index c6d69789bae..b0af3fbe416 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 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 @@ -14,14 +14,16 @@ from __future__ import annotations -from typing import Union, Optional +from typing import Union, Optional, TYPE_CHECKING import numpy as np from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.synthesis.evolution import EvolutionSynthesis, LieTrotter from qiskit.quantum_info import Pauli, SparsePauliOp +if TYPE_CHECKING: + from qiskit.synthesis.evolution import EvolutionSynthesis + class PauliEvolutionGate(Gate): r"""Time-evolution of an operator consisting of Paulis. @@ -107,6 +109,8 @@ class docstring for an example. operator = _to_sparse_pauli_op(operator) if synthesis is None: + from qiskit.synthesis.evolution import LieTrotter + synthesis = LieTrotter() if label is None: diff --git a/qiskit/circuit/random/__init__.py b/qiskit/circuit/random/__init__.py index 3e3dc752d5a..06e817bb4de 100644 --- a/qiskit/circuit/random/__init__.py +++ b/qiskit/circuit/random/__init__.py @@ -12,4 +12,4 @@ """Method for generating random circuits.""" -from .utils import random_circuit +from .utils import random_circuit, random_clifford_circuit diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 71809735aa8..fc497a300cb 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017. +# (C) Copyright IBM 2017, 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 @@ -207,3 +207,72 @@ def random_circuit( qc.measure(qc.qubits, cr) return qc + + +def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): + """Generate a pseudo-random Clifford circuit. + + This function will generate a Clifford circuit by randomly selecting the chosen amount of Clifford + gates from the set of standard gates in :mod:`qiskit.circuit.library.standard_gates`. For example: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') + + Args: + num_qubits (int): number of quantum wires. + num_gates (int): number of gates in the circuit. + gates (list[str]): optional list of Clifford gate names to randomly sample from. + If ``"all"`` (default), use all Clifford gates in the standard library. + seed (int | np.random.Generator): sets random seed/generator (optional). + + Returns: + QuantumCircuit: constructed circuit + """ + + gates_1q = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg"] + gates_2q = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] + if gates == "all": + if num_qubits == 1: + gates = gates_1q + else: + gates = gates_1q + gates_2q + + instructions = { + "i": (standard_gates.IGate(), 1), + "x": (standard_gates.XGate(), 1), + "y": (standard_gates.YGate(), 1), + "z": (standard_gates.ZGate(), 1), + "h": (standard_gates.HGate(), 1), + "s": (standard_gates.SGate(), 1), + "sdg": (standard_gates.SdgGate(), 1), + "sx": (standard_gates.SXGate(), 1), + "sxdg": (standard_gates.SXdgGate(), 1), + "cx": (standard_gates.CXGate(), 2), + "cy": (standard_gates.CYGate(), 2), + "cz": (standard_gates.CZGate(), 2), + "swap": (standard_gates.SwapGate(), 2), + "iswap": (standard_gates.iSwapGate(), 2), + "ecr": (standard_gates.ECRGate(), 2), + "dcx": (standard_gates.DCXGate(), 2), + } + + if isinstance(seed, np.random.Generator): + rng = seed + else: + rng = np.random.default_rng(seed) + + samples = rng.choice(gates, num_gates) + + circ = QuantumCircuit(num_qubits) + + for name in samples: + gate, nqargs = instructions[name] + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + circ.append(gate, qargs, copy=False) + + return circ diff --git a/qiskit/quantum_info/operators/dihedral/random.py b/qiskit/quantum_info/operators/dihedral/random.py index f339cf98377..4331d618d73 100644 --- a/qiskit/quantum_info/operators/dihedral/random.py +++ b/qiskit/quantum_info/operators/dihedral/random.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 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 @@ -49,7 +49,9 @@ def random_cnotdihedral(num_qubits, seed=None): # Random affine function # Random invertible binary matrix - from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.synthesis.linear import ( # pylint: disable=cyclic-import + random_invertible_binary_matrix, + ) linear = random_invertible_binary_matrix(num_qubits, seed=rng) elem.linear = linear diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index cbc54f16bb0..50ffcb74316 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -76,11 +76,11 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: pos = [qubit, qubit + num_qubits] circ = _decompose_clifford_1q(clifford.tableau[pos][:, pos + [-1]]) if len(circ) > 0: - ret_circ.append(circ, [qubit]) + ret_circ.append(circ, [qubit], copy=False) # Add the inverse of the 2-qubit reductions circuit if len(inv_circuit) > 0: - ret_circ.append(inv_circuit.inverse(), range(num_qubits)) + ret_circ.append(inv_circuit.inverse(), range(num_qubits), copy=False) return ret_circ.decompose() diff --git a/qiskit/synthesis/clifford/clifford_decompose_layers.py b/qiskit/synthesis/clifford/clifford_decompose_layers.py index f1a7c5cce13..2fc9ca5bdb2 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_layers.py +++ b/qiskit/synthesis/clifford/clifford_decompose_layers.py @@ -137,32 +137,32 @@ def synth_clifford_layers( cz_func_reverse_qubits=cz_func_reverse_qubits, ) - layeredCircuit.append(S2_circ, qubit_list) + layeredCircuit.append(S2_circ, qubit_list, copy=False) if cx_cz_synth_func is None: - layeredCircuit.append(CZ2_circ, qubit_list) + layeredCircuit.append(CZ2_circ, qubit_list, copy=False) CXinv = CX_circ.copy().inverse() - layeredCircuit.append(CXinv, qubit_list) + layeredCircuit.append(CXinv, qubit_list, copy=False) else: # note that CZ2_circ is None and built into the CX_circ when # cx_cz_synth_func is not None - layeredCircuit.append(CX_circ, qubit_list) + layeredCircuit.append(CX_circ, qubit_list, copy=False) - layeredCircuit.append(H2_circ, qubit_list) - layeredCircuit.append(S1_circ, qubit_list) - layeredCircuit.append(CZ1_circ, qubit_list) + layeredCircuit.append(H2_circ, qubit_list, copy=False) + layeredCircuit.append(S1_circ, qubit_list, copy=False) + layeredCircuit.append(CZ1_circ, qubit_list, copy=False) if cz_func_reverse_qubits: H1_circ = H1_circ.reverse_bits() - layeredCircuit.append(H1_circ, qubit_list) + layeredCircuit.append(H1_circ, qubit_list, copy=False) # Add Pauli layer to fix the Clifford phase signs clifford_target = Clifford(layeredCircuit) pauli_circ = _calc_pauli_diff(cliff, clifford_target) - layeredCircuit.append(pauli_circ, qubit_list) + layeredCircuit.append(pauli_circ, qubit_list, copy=False) return layeredCircuit diff --git a/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml new file mode 100644 index 00000000000..7f2e20db652 --- /dev/null +++ b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml @@ -0,0 +1,14 @@ +--- +features_circuits: + - | + Added a new function to ``qiskit.circuit.random`` that allows to generate a pseudo-random + Clifford circuit with gates from the standard library: :func:`.random_clifford_circuit`. + Example usage: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index f23c0155bc6..3585efb9f64 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2023. +# (C) Copyright IBM 2017, 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 @@ -17,7 +17,9 @@ import numpy as np from ddt import ddt -from qiskit.circuit import Gate, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate, QuantumCircuit +from qiskit.circuit.random import random_clifford_circuit + from qiskit.circuit.library import ( CPhaseGate, CRXGate, @@ -26,7 +28,6 @@ CXGate, CYGate, CZGate, - DCXGate, ECRGate, HGate, IGate, @@ -37,10 +38,7 @@ RYYGate, RZZGate, RZXGate, - SdgGate, SGate, - SXGate, - SXdgGate, SwapGate, XGate, XXMinusYYGate, @@ -57,98 +55,11 @@ from qiskit.quantum_info.operators import Clifford, Operator from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.symplectic.clifford_circuits import _append_operation -from qiskit.synthesis.clifford import ( - synth_clifford_full, - synth_clifford_ag, - synth_clifford_bm, - synth_clifford_greedy, -) from qiskit.synthesis.linear import random_invertible_binary_matrix from test import QiskitTestCase # pylint: disable=wrong-import-order from test import combine # pylint: disable=wrong-import-order -class VGate(Gate): - """V Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new V Gate.""" - super().__init__("v", 1, []) - - def _define(self): - """V Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.sdg(0) - qc.h(0) - self.definition = qc - - -class WGate(Gate): - """W Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new W Gate.""" - super().__init__("w", 1, []) - - def _define(self): - """W Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.append(VGate(), [q[0]], []) - qc.append(VGate(), [q[0]], []) - self.definition = qc - - -def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): - """Generate a pseudo random Clifford circuit.""" - - qubits_1_gates = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg", "v", "w"] - qubits_2_gates = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] - if gates == "all": - if num_qubits == 1: - gates = qubits_1_gates - else: - gates = qubits_1_gates + qubits_2_gates - - instructions = { - "i": (IGate(), 1), - "x": (XGate(), 1), - "y": (YGate(), 1), - "z": (ZGate(), 1), - "h": (HGate(), 1), - "s": (SGate(), 1), - "sdg": (SdgGate(), 1), - "sx": (SXGate(), 1), - "sxdg": (SXdgGate(), 1), - "v": (VGate(), 1), - "w": (WGate(), 1), - "cx": (CXGate(), 2), - "cy": (CYGate(), 2), - "cz": (CZGate(), 2), - "swap": (SwapGate(), 2), - "iswap": (iSwapGate(), 2), - "ecr": (ECRGate(), 2), - "dcx": (DCXGate(), 2), - } - - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) - - samples = rng.choice(gates, num_gates) - - circ = QuantumCircuit(num_qubits) - - for name in samples: - gate, nqargs = instructions[name] - qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - circ.append(gate, qargs) - - return circ - - @ddt class TestCliffordGates(QiskitTestCase): """Tests for clifford append gate functions.""" @@ -588,92 +499,6 @@ def test_from_circuit_with_all_types(self): self.assertEqual(combined_clifford, expected_clifford) -@ddt -class TestCliffordSynthesis(QiskitTestCase): - """Test Clifford synthesis methods.""" - - @staticmethod - def _cliffords_1q(): - clifford_dicts = [ - {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, - ] - return [Clifford.from_dict(i) for i in clifford_dicts] - - def test_decompose_1q(self): - """Test synthesis for all 1-qubit Cliffords""" - for cliff in self._cliffords_1q(): - with self.subTest(msg=f"Test circuit {cliff}"): - target = cliff - value = Clifford(cliff.to_circuit()) - self.assertEqual(target, value) - - @combine(num_qubits=[2, 3]) - def test_synth_bm(self, num_qubits): - """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_bm(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[2, 3, 4, 5]) - def test_synth_ag(self, num_qubits): - """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_ag(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_greedy(self, num_qubits): - """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_greedy(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_full(self, num_qubits): - """Test synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_full(target)) - self.assertEqual(value, target) - - @ddt class TestCliffordDecomposition(QiskitTestCase): """Test Clifford decompositions.""" @@ -683,11 +508,9 @@ class TestCliffordDecomposition(QiskitTestCase): ["h", "s"], ["h", "s", "i", "x", "y", "z"], ["h", "s", "sdg"], - ["h", "s", "v"], - ["h", "s", "w"], ["h", "sx", "sxdg"], ["s", "sx", "sxdg"], - ["h", "s", "sdg", "i", "x", "y", "z", "v", "w", "sx", "sxdg"], + ["h", "s", "sdg", "i", "x", "y", "z", "sx", "sxdg"], ] ) def test_to_operator_1qubit_gates(self, gates): diff --git a/test/python/synthesis/test_clifford_sythesis.py b/test/python/synthesis/test_clifford_sythesis.py new file mode 100644 index 00000000000..887f1af5ad9 --- /dev/null +++ b/test/python/synthesis/test_clifford_sythesis.py @@ -0,0 +1,118 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +# pylint: disable=invalid-name +"""Tests for Clifford synthesis functions.""" + +import numpy as np +from ddt import ddt +from qiskit.circuit.random import random_clifford_circuit +from qiskit.quantum_info.operators import Clifford +from qiskit.synthesis.clifford import ( + synth_clifford_full, + synth_clifford_ag, + synth_clifford_bm, + synth_clifford_greedy, +) + +from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import combine # pylint: disable=wrong-import-order + + +@ddt +class TestCliffordSynthesis(QiskitTestCase): + """Tests for clifford synthesis functions.""" + + @staticmethod + def _cliffords_1q(): + clifford_dicts = [ + {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, + ] + return [Clifford.from_dict(i) for i in clifford_dicts] + + def test_decompose_1q(self): + """Test synthesis for all 1-qubit Cliffords""" + for cliff in self._cliffords_1q(): + with self.subTest(msg=f"Test circuit {cliff}"): + target = cliff + value = Clifford(cliff.to_circuit()) + self.assertEqual(target, value) + + @combine(num_qubits=[2, 3]) + def test_synth_bm(self, num_qubits): + """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_bm(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[2, 3, 4, 5]) + def test_synth_ag(self, num_qubits): + """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 1 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_ag(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_greedy(self, num_qubits): + """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_greedy(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_full(self, num_qubits): + """Test synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_full(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) From 6a872a8e48e642631c73ee37ec4b949d98386d16 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 7 May 2024 17:59:41 +0300 Subject: [PATCH 045/159] docstring fixes (#12358) --- qiskit/quantum_info/operators/symplectic/pauli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index e1bcfa29ebc..1ccecc04a6c 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -144,13 +144,13 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be .. code-block:: python - p = Pauli('-iXYZ') + P = Pauli('-iXYZ') print('P[0] =', repr(P[0])) print('P[1] =', repr(P[1])) print('P[2] =', repr(P[2])) print('P[:] =', repr(P[:])) - print('P[::-1] =, repr(P[::-1])) + print('P[::-1] =', repr(P[::-1])) """ # Set the max Pauli string size before truncation From 0f7424cb144cf985107273780b809091ceff0432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 00:31:25 +0100 Subject: [PATCH 046/159] Bump num-bigint from 0.4.4 to 0.4.5 (#12357) Bumps [num-bigint](https://github.com/rust-num/num-bigint) from 0.4.4 to 0.4.5. - [Changelog](https://github.com/rust-num/num-bigint/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-bigint/compare/num-bigint-0.4.4...num-bigint-0.4.5) --- updated-dependencies: - dependency-name: num-bigint 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 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8b22ed7917..33114124e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,11 +724,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] From b5c5179a0da209911dae69916bc1454def851a3e Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 8 May 2024 01:37:22 -0400 Subject: [PATCH 047/159] fix indentation in XXPlusYYGate docstring (#12365) * fix indentation in XXPlusYYGate docstring * break long lines --- .../library/standard_gates/xx_plus_yy.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index a7b62175f20..a82316ed7b0 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -71,18 +71,20 @@ class XXPlusYYGate(Gate): q_1: ┤0 ├ └───────────────┘ - .. math:: - - \newcommand{\rotationangle}{\frac{\theta}{2}} - - R_{XX+YY}(\theta, \beta)\ q_0, q_1 = - RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos\left(\rotationangle\right) & -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ - 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & \cos\left(\rotationangle\right) & 0 \\ - 0 & 0 & 0 & 1 - \end{pmatrix} + .. math:: + + \newcommand{\rotationangle}{\frac{\theta}{2}} + + R_{XX+YY}(\theta, \beta)\ q_0, q_1 = + RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos\left(\rotationangle\right) & + -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ + 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & + \cos\left(\rotationangle\right) & 0 \\ + 0 & 0 & 0 & 1 + \end{pmatrix} """ def __init__( From 8b94fc36df05a9f5b2150e7a8ac1c36dca3286d1 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Wed, 8 May 2024 12:13:16 +0300 Subject: [PATCH 048/159] Replace initialization method by Isometry in StatePreparation (#12178) * replace initializetion method by Isometry in StatePreparation * add names to QuantumRegister and QuantumCircuit * update docs to the new reference * remove old initialization code based on Shende et al * fix reference in docs * add release notes * fix lint errors * fix references in docs * add a benchmark for state preparation * update circuit name following review --- .../library/data_preparation/initializer.py | 8 + .../data_preparation/state_preparation.py | 208 ++---------------- .../library/generalized_gates/isometry.py | 16 +- .../generalized_gates/mcg_up_to_diagonal.py | 8 +- .../circuit/library/generalized_gates/uc.py | 4 +- .../library/generalized_gates/uc_pauli_rot.py | 4 +- ...lgorithm-by-isometry-41f9ffa58f72ece5.yaml | 7 + test/benchmarks/statepreparation.py | 66 ++++++ 8 files changed, 114 insertions(+), 207 deletions(-) create mode 100644 releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml create mode 100644 test/benchmarks/statepreparation.py diff --git a/qiskit/circuit/library/data_preparation/initializer.py b/qiskit/circuit/library/data_preparation/initializer.py index 394f863191d..0e38f067403 100644 --- a/qiskit/circuit/library/data_preparation/initializer.py +++ b/qiskit/circuit/library/data_preparation/initializer.py @@ -36,6 +36,14 @@ class Initialize(Instruction): the :class:`~.library.StatePreparation` class. Note that ``Initialize`` is an :class:`~.circuit.Instruction` and not a :class:`.Gate` since it contains a reset instruction, which is not unitary. + + The initial state is prepared based on the :class:`~.library.Isometry` synthesis described in [1]. + + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. + """ def __init__( diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 2d48e5cd077..43e80ead883 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Prepare a quantum state from the state where all qubits are 0.""" -import cmath from typing import Union, Optional import math @@ -21,11 +20,10 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.gate import Gate -from qiskit.circuit.library.standard_gates.x import CXGate, XGate +from qiskit.circuit.library.standard_gates.x import XGate from qiskit.circuit.library.standard_gates.h import HGate from qiskit.circuit.library.standard_gates.s import SGate, SdgGate -from qiskit.circuit.library.standard_gates.ry import RYGate -from qiskit.circuit.library.standard_gates.rz import RZGate +from qiskit.circuit.library.generalized_gates import Isometry from qiskit.circuit.exceptions import CircuitError from qiskit.quantum_info.states.statevector import Statevector # pylint: disable=cyclic-import @@ -71,13 +69,13 @@ def __init__( Raises: QiskitError: ``num_qubits`` parameter used when ``params`` is not an integer - When a Statevector argument is passed the state is prepared using a recursive - initialization algorithm, including optimizations, from [1], as well - as some additional optimizations including removing zero rotations and double cnots. + When a Statevector argument is passed the state is prepared based on the + :class:`~.library.Isometry` synthesis described in [1]. - **References:** - [1] Shende, Bullock, Markov. Synthesis of Quantum Logic Circuits (2004) - [`https://arxiv.org/abs/quant-ph/0406176v5`] + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ self._params_arg = params @@ -119,7 +117,7 @@ def _define(self): elif self._from_int: self.definition = self._define_from_int() else: - self.definition = self._define_synthesis() + self.definition = self._define_synthesis_isom() def _define_from_label(self): q = QuantumRegister(self.num_qubits, "q") @@ -168,29 +166,18 @@ def _define_from_int(self): # we don't need to invert anything return initialize_circuit - def _define_synthesis(self): - """Calculate a subcircuit that implements this initialization - - Implements a recursive initialization algorithm, including optimizations, - from "Synthesis of Quantum Logic Circuits" Shende, Bullock, Markov - https://arxiv.org/abs/quant-ph/0406176v5 + def _define_synthesis_isom(self): + """Calculate a subcircuit that implements this initialization via isometry""" + q = QuantumRegister(self.num_qubits, "q") + initialize_circuit = QuantumCircuit(q, name="init_def") - Additionally implements some extra optimizations: remove zero rotations and - double cnots. - """ - # call to generate the circuit that takes the desired vector to zero - disentangling_circuit = self._gates_to_uncompute() + isom = Isometry(self._params_arg, 0, 0) + initialize_circuit.append(isom, q[:]) # invert the circuit to create the desired vector from zero (assuming # the qubits are in the zero state) - if self._inverse is False: - initialize_instr = disentangling_circuit.to_instruction().inverse() - else: - initialize_instr = disentangling_circuit.to_instruction() - - q = QuantumRegister(self.num_qubits, "q") - initialize_circuit = QuantumCircuit(q, name="init_def") - initialize_circuit.append(initialize_instr, q[:]) + if self._inverse is True: + return initialize_circuit.inverse() return initialize_circuit @@ -253,164 +240,3 @@ def validate_parameter(self, parameter): def _return_repeat(self, exponent: float) -> "Gate": return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) - - def _gates_to_uncompute(self): - """Call to create a circuit with gates that take the desired vector to zero. - - Returns: - QuantumCircuit: circuit to take self.params vector to :math:`|{00\\ldots0}\\rangle` - """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q, name="disentangler") - - # kick start the peeling loop, and disentangle one-by-one from LSB to MSB - remaining_param = self.params - - for i in range(self.num_qubits): - # work out which rotations must be done to disentangle the LSB - # qubit (we peel away one qubit at a time) - (remaining_param, thetas, phis) = StatePreparation._rotations_to_disentangle( - remaining_param - ) - - # perform the required rotations to decouple the LSB qubit (so that - # it can be "factored" out, leaving a shorter amplitude vector to peel away) - - add_last_cnot = True - if np.linalg.norm(phis) != 0 and np.linalg.norm(thetas) != 0: - add_last_cnot = False - - if np.linalg.norm(phis) != 0: - rz_mult = self._multiplex(RZGate, phis, last_cnot=add_last_cnot) - circuit.append(rz_mult.to_instruction(), q[i : self.num_qubits]) - - if np.linalg.norm(thetas) != 0: - ry_mult = self._multiplex(RYGate, thetas, last_cnot=add_last_cnot) - circuit.append(ry_mult.to_instruction().reverse_ops(), q[i : self.num_qubits]) - circuit.global_phase -= np.angle(sum(remaining_param)) - return circuit - - @staticmethod - def _rotations_to_disentangle(local_param): - """ - Static internal method to work out Ry and Rz rotation angles used - to disentangle the LSB qubit. - These rotations make up the block diagonal matrix U (i.e. multiplexor) - that disentangles the LSB. - - [[Ry(theta_1).Rz(phi_1) 0 . . 0], - [0 Ry(theta_2).Rz(phi_2) . 0], - . - . - 0 0 Ry(theta_2^n).Rz(phi_2^n)]] - """ - remaining_vector = [] - thetas = [] - phis = [] - - param_len = len(local_param) - - for i in range(param_len // 2): - # Ry and Rz rotations to move bloch vector from 0 to "imaginary" - # qubit - # (imagine a qubit state signified by the amplitudes at index 2*i - # and 2*(i+1), corresponding to the select qubits of the - # multiplexor being in state |i>) - (remains, add_theta, add_phi) = StatePreparation._bloch_angles( - local_param[2 * i : 2 * (i + 1)] - ) - - remaining_vector.append(remains) - - # rotations for all imaginary qubits of the full vector - # to move from where it is to zero, hence the negative sign - thetas.append(-add_theta) - phis.append(-add_phi) - - return remaining_vector, thetas, phis - - @staticmethod - def _bloch_angles(pair_of_complex): - """ - Static internal method to work out rotation to create the passed-in - qubit from the zero vector. - """ - [a_complex, b_complex] = pair_of_complex - # Force a and b to be complex, as otherwise numpy.angle might fail. - a_complex = complex(a_complex) - b_complex = complex(b_complex) - mag_a = abs(a_complex) - final_r = math.sqrt(mag_a**2 + abs(b_complex) ** 2) - if final_r < _EPS: - theta = 0 - phi = 0 - final_r = 0 - final_t = 0 - else: - theta = 2 * math.acos(mag_a / final_r) - a_arg = cmath.phase(a_complex) - b_arg = cmath.phase(b_complex) - final_t = a_arg + b_arg - phi = b_arg - a_arg - - return final_r * cmath.exp(1.0j * final_t / 2), theta, phi - - def _multiplex(self, target_gate, list_of_angles, last_cnot=True): - """ - Return a recursive implementation of a multiplexor circuit, - where each instruction itself has a decomposition based on - smaller multiplexors. - - The LSB is the multiplexor "data" and the other bits are multiplexor "select". - - Args: - target_gate (Gate): Ry or Rz gate to apply to target qubit, multiplexed - over all other "select" qubits - list_of_angles (list[float]): list of rotation angles to apply Ry and Rz - last_cnot (bool): add the last cnot if last_cnot = True - - Returns: - DAGCircuit: the circuit implementing the multiplexor's action - """ - list_len = len(list_of_angles) - local_num_qubits = int(math.log2(list_len)) + 1 - - q = QuantumRegister(local_num_qubits) - circuit = QuantumCircuit(q, name="multiplex" + str(local_num_qubits)) - - lsb = q[0] - msb = q[local_num_qubits - 1] - - # case of no multiplexing: base case for recursion - if local_num_qubits == 1: - circuit.append(target_gate(list_of_angles[0]), [q[0]]) - return circuit - - # calc angle weights, assuming recursion (that is the lower-level - # requested angles have been correctly implemented by recursion - angle_weight = np.kron([[0.5, 0.5], [0.5, -0.5]], np.identity(2 ** (local_num_qubits - 2))) - - # calc the combo angles - list_of_angles = angle_weight.dot(np.array(list_of_angles)).tolist() - - # recursive step on half the angles fulfilling the above assumption - multiplex_1 = self._multiplex(target_gate, list_of_angles[0 : (list_len // 2)], False) - circuit.append(multiplex_1.to_instruction(), q[0:-1]) - - # attach CNOT as follows, thereby flipping the LSB qubit - circuit.append(CXGate(), [msb, lsb]) - - # implement extra efficiency from the paper of cancelling adjacent - # CNOTs (by leaving out last CNOT and reversing (NOT inverting) the - # second lower-level multiplex) - multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2) :], False) - if list_len > 1: - circuit.append(multiplex_2.to_instruction().reverse_ops(), q[0:-1]) - else: - circuit.append(multiplex_2.to_instruction(), q[0:-1]) - - # attach a final CNOT - if last_cnot: - circuit.append(CXGate(), [msb, lsb]) - - return circuit diff --git a/qiskit/circuit/library/generalized_gates/isometry.py b/qiskit/circuit/library/generalized_gates/isometry.py index c180e7a1448..e6b4f6fb21c 100644 --- a/qiskit/circuit/library/generalized_gates/isometry.py +++ b/qiskit/circuit/library/generalized_gates/isometry.py @@ -45,10 +45,10 @@ class Isometry(Instruction): The decomposition is based on [1]. - **References:** - - [1] Iten et al., Quantum circuits for isometries (2016). - `Phys. Rev. A 93, 032318 `__. + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ @@ -123,8 +123,8 @@ def _define(self): # later here instead. gate = self.inv_gate() gate = gate.inverse() - q = QuantumRegister(self.num_qubits) - iso_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + iso_circuit = QuantumCircuit(q, name="isometry") iso_circuit.append(gate, q[:]) self.definition = iso_circuit @@ -139,8 +139,8 @@ def _gates_to_uncompute(self): Call to create a circuit with gates that take the desired isometry to the first 2^m columns of the 2^n*2^n identity matrix (see https://arxiv.org/abs/1501.06911) """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="isometry_to_uncompute") ( q_input, q_ancillas_for_output, diff --git a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py index 49d7dc36958..b95ec6f63e3 100644 --- a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py +++ b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py @@ -68,8 +68,8 @@ def __init__( def _define(self): mcg_up_diag_circuit, _ = self._dec_mcg_up_diag() gate = mcg_up_diag_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) - mcg_up_diag_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + mcg_up_diag_circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") mcg_up_diag_circuit.append(gate, q[:]) self.definition = mcg_up_diag_circuit @@ -108,8 +108,8 @@ def _dec_mcg_up_diag(self): q=[q_target,q_controls,q_ancilla_zero,q_ancilla_dirty] """ diag = np.ones(2 ** (self.num_controls + 1)).tolist() - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") (q_target, q_controls, q_ancillas_zero, q_ancillas_dirty) = self._define_qubit_role(q) # ToDo: Keep this threshold updated such that the lowest gate count is achieved: # ToDo: we implement the MCG with a UCGate up to diagonal if the number of controls is diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index 6e6a1db95ca..c81494da3ee 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -148,10 +148,10 @@ def _dec_ucg(self): the diagonal gate is also returned. """ diag = np.ones(2**self.num_qubits).tolist() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") q_controls = q[1:] q_target = q[0] - circuit = QuantumCircuit(q) + circuit = QuantumCircuit(q, name="uc") # If there is no control, we use the ZYZ decomposition if not q_controls: circuit.unitary(self.params[0], [q]) diff --git a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py index 5b5633ec423..6b637f7d2b2 100644 --- a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py +++ b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py @@ -69,7 +69,7 @@ def __init__(self, angle_list: list[float], rot_axis: str) -> None: def _define(self): ucr_circuit = self._dec_ucrot() gate = ucr_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") ucr_circuit = QuantumCircuit(q) ucr_circuit.append(gate, q[:]) self.definition = ucr_circuit @@ -79,7 +79,7 @@ def _dec_ucrot(self): Finds a decomposition of a UC rotation gate into elementary gates (C-NOTs and single-qubit rotations). """ - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") circuit = QuantumCircuit(q) q_target = q[0] q_controls = q[1:] diff --git a/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml new file mode 100644 index 00000000000..5bf8e7a80b4 --- /dev/null +++ b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml @@ -0,0 +1,7 @@ +--- +features_circuits: + - | + Replacing the internal synthesis algorithm of :class:`~.library.StatePreparation` + and :class:`~.library.Initialize` of Shende et al. by the algorithm given in + :class:`~.library.Isometry` of Iten et al. + The new algorithm reduces the number of CX gates and the circuit depth by a factor of 2. diff --git a/test/benchmarks/statepreparation.py b/test/benchmarks/statepreparation.py new file mode 100644 index 00000000000..67dc1178fc2 --- /dev/null +++ b/test/benchmarks/statepreparation.py @@ -0,0 +1,66 @@ +# 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. + +# pylint: disable=missing-docstring,invalid-name,no-member +# pylint: disable=attribute-defined-outside-init +# pylint: disable=unused-argument + +import numpy as np +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.compiler import transpile +from qiskit.circuit.library.data_preparation import StatePreparation + + +class StatePreparationTranspileBench: + params = [4, 5, 6, 7, 8] + param_names = ["number of qubits in state"] + + def setup(self, n): + q = QuantumRegister(n) + qc = QuantumCircuit(q) + state = np.random.rand(2**n) + np.random.rand(2**n) * 1j + state = state / np.linalg.norm(state) + state_gate = StatePreparation(state) + qc.append(state_gate, q) + + self.circuit = qc + + def track_cnot_counts_after_mapping_to_ibmq_16_melbourne(self, *unused): + coupling = [ + [1, 0], + [1, 2], + [2, 3], + [4, 3], + [4, 10], + [5, 4], + [5, 6], + [5, 9], + [6, 8], + [7, 8], + [9, 8], + [9, 10], + [11, 3], + [11, 10], + [11, 12], + [12, 2], + [13, 1], + [13, 12], + ] + circuit = transpile( + self.circuit, + basis_gates=["u1", "u3", "u2", "cx"], + coupling_map=coupling, + seed_transpiler=0, + ) + counts = circuit.count_ops() + cnot_count = counts.get("cx", 0) + return cnot_count From be1d24a739b3e78316ef592ebd06188bb9824d2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 12:24:10 +0000 Subject: [PATCH 049/159] Bump num-complex from 0.4.5 to 0.4.6 (#12368) Bumps [num-complex](https://github.com/rust-num/num-complex) from 0.4.5 to 0.4.6. - [Changelog](https://github.com/rust-num/num-complex/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-complex/compare/num-complex-0.4.5...num-complex-0.4.6) --- updated-dependencies: - dependency-name: num-complex 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 33114124e1e..b6915167f9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,9 +734,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", From 87c14cb2bf9cfd9ae12985de994b6c3a31edfa61 Mon Sep 17 00:00:00 2001 From: Derek Miner Date: Wed, 8 May 2024 09:42:18 -0500 Subject: [PATCH 050/159] Add new method for single bitstring target when measuring stabilizerstate probabilities_dict (#12147) * added new method, deterministic helper unction added new method probabilities_dict_from_bitstrings, moved logic from probabilities_dict to probabilities_dict_from_bitstrings with ability to pass target, probabilities_dict will pass a target of None. * Adding test timing, fixed algorithm for determining if should get probability for qubit Added some timing of the tests to determine if the version with a specfic target runs faster then the non-targetted version (which must calculate all 2^n possibilities for stabilizer state test_probablities_dict_single_qubit. When given a target it can skip branches that were not wanting to be calculated * Adding more tests, fixing deterministic issues with bad target * Prob of targets with 0.0 probability * target calc changes * Simpler way to get all targets * Need to improve performance with multiple targets * Simplified the probabilities_dict_from_bitstrings method simplified the method probabilities_dict_from_bitstrings, less checking was needed, simpler handling of no target value passed in * Adding tests for targets * Added tests to test_probablities_dict_qubits * Add performance boost check when using targets for test_probablities_dict_qubits * Added Caching to prevent duplicate branch calculations In stabilizerstate, added the ability to cache previously calculated branches so they do not get recalculated when attempting to get target values, gives a better starting point to calculate from instead of starting from an unknown probability value of 1.0 * improve performance by performing expensive operations only when needed when creating an array for the outcome in probability, it is only built if the cache key is not found * Optimization for caching, deterministic calcs for branches, enabling/disabling cache added more optimization for performance when performing calculations for branches using caching and targets, it will now also store the deterministic node values which gives a very slight performance increase, migrated to most efficient ways to store and retrieve cache values. The performance increase when using a large amount of qubits is huge, when using a very small amount of qubits the very slight over head of caching makes it about the same. Increasing test coverage for using targets with probabilities * improved tests, checking of caching vs not caching implemented more accurate tests that verify probability caching * added more tests and variations for stabilizerstate * Corrected deterministic probability calculations * Fixed bug with cache in wrong place for some calculations for some calculations the cached results were in the wrong place which could cause false results for certain test cases with 3+ qubits * added more test cases * Probabilities update for moved method call * Added test for random test_probs_random_subsystem * fixed single probabilities tests lost in previous merge * Removed caching of deterministic values overhead was too high compared to calculating * Fixed failing test with test_probs_random_subsystem Fixed failing test that would randomly occur, the stabilizer state was not being cached as well, which would sometimes produce a failure. After caching the object to restore when calculating starting at a node down the branch, this failure no longer occurs. Need to optimize and clean up the code a bit, but functioning * Adding tests back that were not test_probs_random_subsystem, removed to test * Created ProbabilityCache object for dealing with caching * Finished adding ProbabilityCache object and commenting * Added more documentation for stabilizer state * Fixing style with tox -eblack * commenting fixes in stabilizerstate * Added release note * Updated release notes * added __future__ to probabilitiescache, removed unused import stabilizerstate * run tox -eblack to correct probabilitiescache * Corrected lint issues in multiple classes fixed lint issues in test_probabilitiycache, test_stabilizerstate, stabilizerstate, and probabilitycache * fixed more pylint lint issues pylint -rn qiskit test tools Corrected in probabilitycache, stabilizerstate, test_probabilitycach, test_stabilizerstate * fixed more lint issues fixed with: tox -eblack verified with: black --check qiskit test tools examples setup.py pylint -rn qiskit test tools tox -elint * added a bit more variance allowance for performance tests renamed test_probabilitycache.py file to correct spelling added a bit more performance_varability_percent percent time allowed from 0.001 to 0.005 * Corrected Lint issues, added test for probabilitycache added test_probabilitycache test, fixed lint issues and type issues in stabilizerstate, test_stabilizerstate * Fixed remaining import ordering issue * readded ddt decorator for test_probabilitycache * Changed to using process_time_ns from monotonic for performance measuring * Added output when performance times fail to aid in debugging in test_stabilizerstate * moved performance end time out of same method to calculate times * changed method name in test_stabilizerstate changed probability_percent_of_calculated_branches to _probability_percent_of_calculated_branches * Added GC disabling between timed tests * Attempting using perf_counter_ns instead of process_time_ns * Updated commenting, moved 'X' outcome check moved 'X' in outcome out of the for loop for StabilizerState._get_probabilities to help performance * more comment updating * Changed performance timer based on OS type * Changing to time.thread_time_ns(), and raising performance varabilitiy by 0.005 using time.thread_time_ns() for benchmarking instead of time.perf_counter_ns() and time.process_time_ns() based on the OS in test_stabilizerstate. It seems that time is being counted that shouldn't be with the other methods that causes false reports of the work taking longer then it does. Raising the performance_varability_percent in test_stablilizerstate by 0.005 to 0.01 for more headroom for performance checking * changing time.thread_time() vs time.thread_time_ns() to see if compatible with windows also added one more target for test_4 to differentiate no caching against caching in test_stabilizerstate * Changing performance timer based on OS for test_stablizerstate perf_counter_ns for win32 and thread_time for all other OS * fixed small issue in probabilitycache, improved probabilitycache test fixed issue when checking the cache key that was not operating correctly, internally the key is always a str, but when a list[str] was passed in the check was not correct, so changed to check if an instance of a list, and will join the outcome val to a str for the internal cache key. Improved the probabilitycache to test having alternating keys of str and list[str] to test this internal key checking * Added correct type hinting to probabilitycache * removed typing hint module removed the use of typing module for type hinting, looks like python wants to get away from using this module for type hinting, also fixed some commenting issues in stabilizerstate, test_probabilitycache, test_stabilizerstate, and probabilitycache files * Fix test pylint failure of Value 'list' is unsubscriptable (unsubscriptable-object) fixing tests failing with pylint when lint checking is performed. This worked locally and wasn't failing, but when it runs in the CI/CD process it fails, so adding from __future__ import annotations to avoid this failure for test_probabilitycache and test_stabilizerstate as new tests include type hinting * Fix small comment error in test, Added use_caching to tests, simplified cache calls Fixed a small error in my commenting for the test_stabilizerstate.py and forced use_caching parm in the call of probabilities_dict_from_bitstrings in the tests, even tho it is used as default, to make sure it is clear in the tests. Simplfied the adding values to Probabilitycache, and simplified the code in StabilizerState._get_probabilities() setting values in cache and retrieving the state when using cache, made cache_key in probabilitycache "private" and changed method to _cache_key * Significant code refactoring in StabiizerState Significant code refactoring in stabilizerstate. I was able to condense the use the cache to a smaller area in the _get_probabilities helper method so it is easier to work with and more clear. I removed methods I created that were single use helper methods that were able to be condensed down into simpler code such as _branches_to_measure, _retrieve_deterministic_probability, _branches_to_measure, _is_qubit_deterministic. Since these are 1 time use methods it didn't make sense to keep them around. The _get_probabilities helper method is much simpler and clearer, closer to the original with less tweaks and functioning with the same performance, and more clarity. Most of the code changed at this point is adding tests, most of this commit was removing unnecessary code and refactoring * Fixed stabilizerstate cache key outcome not found there was a check to make sure a key could be found before that was removed in the refactoring, once and awhile you get a target that can't be found in the cache because of the algorithm of looking for a key by increasing the number of X from the left to the right. There are situations where the X would be in the middle such as 0X1 that won't be found. The algorithm in the probability cache could be changed to use BST which would very rarely help performance, the fix is to make sure a key can be found and then only use the cache if that is the case * Moved all caching retrieval outside of _get_probabilities for performance there is a slight overhead when recursively iterating through the _get_probabilities helper when having to check if caching should be retrieved. Realistically when you use caching it is just to get you a better starting point, so it only needs to be retrieved once. The cache inserting remains in the _get_probabilities helper to build up the cache, but the logic was significantly simplified in the _get_probabilities helper, and now only a 1 time cache retrieval is performed for each target via the probabilities_dict_from_bitstrings method * Simplified Bitstring method for probability cache, removed unnecessary classes Removed Probabilitycache class, test_probabilitiycache.py. Simplified test_stabilizerstate.py for new bitstring probability testing methods. changed method probabilities_dict_from_bitstrings to probabilities_dict_from_bitstring, and simplified by removing cache, and only allowing a single string value to be passed in * Commenting update for _probabilities_bitstring_verify updating the commenting for _probabilities_bitstring_verify, move to top of file * black lint formatter ran * Uncomment out line causing circular import when running locally I get a circular import for two_qubit_decompose.py on line 154 "_specializations = two_qubit_decompose.Specialization", I need to comment out to test locally, but did not mean to commit this. Simplified the stablizierstate self._get_probabilities method but removing an unnecessary check * Remove type hints from test_stabilizerstate * Updated test_stabilizerstate, removed helper test method, new release note update the tests in test_stabilizerstate to no longer use a helper method for checking all the bitstring measurements in. Created a _get_probabilities_dict helper method in stabilizer state to be used for the probabilities_dict and probabilities_dict_with_bitstring methods. This was needed so that they could share the logic of the measurements, but also so that you could enforce in the probabilities_dict_with_bitstring method that outcome_bitstring must be provided. Previouslly this was optional and the user could avoid passing in a value, but this would essentially make it function the same as probabilities_dict. Now with the helper it enforces they must provide a bitstring * Added large qubit test with h gates, reorder imports for test_stabilizerstate added new test method test_probabilities_dict_large_num_qubits with large amount of qubit tests using an outcome bitstring target, num_qubits=[25, 50, 100, 200, 300, 400, 500, 600, 750], normally get any results for 750 qubits would require ~5.922387e+225 full bitstring calculations, but if you want one target bitstring you can perform ~750 measurements for a single probability for one result and get the value quickly * moved methods out of loop that are unnecessary in test_stabilizerstate moded target dict calculation out of loop that is not necessary to recalculate through each sample loop calculation * broke out medium hgate test to own test method, issue with assertTrue type in test testing a variety of hgates that also do the full probabilities_dict calculations up to 9 qubits called test_probabilities_dict_hgate_medium_num_qubits, also changed a assertTrue typo to assertDictEqual in the test_probabilities_dict_large_num_qubits method * fixed commenting in stabilizerstate, removed unnecessary outcome_bitstring array access syntax fixed some commenting for probabilities_dict_from_bitstring, and removed unnecessary syntax for accessing the array, was outcome_bitstring[i : i + 1] now outcome_bitstring[i] in _get_probabilities * Condensed tests in test_stabilizerstate, added helper test class, more test coverage added class StabilizerStateTestingTools in file test_stabilizerstate, these methods needed to be moved outside of the TestStabilizerState class because they are also needed in the TestStabilizerStateExpectationValue class within the file. It did not make sense to put these helper methods in the QiskitTestCase class, or the BaseQiskitTestCase class as these helpers are specific to helping test a new method in the stabilizerstate class. I condensed redudnant code for checking the individual bitstrings into helper method _verify_individual_bitstrings which takes a target dict, uses each entry in the dict to attempt to get the single probability, and verify each, which drastically raises the test coverage of this new method probabilities_dict_from_bitstring. added helper method StabilizerStateTestingTools._bitstring_product which builds a dict of the product of 0, 1 for a lenth passed in, used in several test cases for testing the zero probabilities. In the test cases you will also notice cases where I update the target dict target.update(StabilizerStateTestingTools._bitstring_product( num_qubits, target)), which builds the rest of the bitstring values that will have a 0 probability, which is then passed into the _verify_individual_bitstrings method to verify they get the correct 0 value I reduced the number of qubits for the test_probabilities_dict_hgate_large_num_qubits from tests of num_qubits=[25, 50, 100, 200, 300, 400, 500, 600, 750] to num_qubits=[25, 50, 100, 200] due to memory issues on windows, and the largest use case I have received so far is 100 qubits, so going above that by 2x Overall test coverage should be dramatically increased now testing for 0 probabilities, checking every bitstring individually has a probability, and simplifying the code, removing a lot of repetative logic, moved to the helper methods * Fixed isues in release notes https://github.com/Qiskit/qiskit/pull/12147#discussion_r1578906579 https://github.com/Qiskit/qiskit/pull/12147#discussion_r1578907028 * Corrected Targetted to targeted in stablizerstate * Updated commenting about target https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579358105 https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579356207 * Moved helper function _get_probabilities_dict moved the helper function _get_probabilities_dict to the helper function area next to _get_probabilities https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579365690 * differentiated description for probabilities https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579193692 * Added test to test_stabilizerstate added a test that does not have all equal probabilities and is not a GHZ state https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579395521 * combined 2 test methods combine the large and medium tests into one due to taking too long to run the tests and lowered the amount of qubits * removed unnecessary test removed redundant test in test_probabilities_dict_medium_num_qubits, as this was combined with the large test * fixed lint issues, renamed test method renamed test_probabilities_dict_medium_num_qubits in test_stabilizerstate to test_probabilities_dict_from_bitstring * fixed type for targetting in stabilizerstate typo for description for word targetting, changed to targeting --------- Co-authored-by: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> --- qiskit/quantum_info/states/stabilizerstate.py | 161 +++++++++++--- ...r_probabilities_dict-e53f524d115bbcfc.yaml | 13 ++ .../states/test_stabilizerstate.py | 203 ++++++++++++++++-- 3 files changed, 329 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index 7f616bcff79..4ae16c32bf5 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -386,8 +386,17 @@ def probabilities(self, qargs: None | list = None, decimals: None | int = None) return probs - def probabilities_dict(self, qargs: None | list = None, decimals: None | int = None) -> dict: - """Return the subsystem measurement probability dictionary. + def probabilities_dict_from_bitstring( + self, + outcome_bitstring: str, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary utilizing + a targeted outcome_bitstring to perform the measurement for. This + will calculate a probability for only a single targeted + outcome_bitstring value, giving a performance boost over calculating + all possible outcomes. Measurement probabilities are with respect to measurement in the computation (diagonal) basis. @@ -398,30 +407,44 @@ def probabilities_dict(self, qargs: None | list = None, decimals: None | int = N inserted between integers so that subsystems can be distinguished. Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) qargs (None or list): subsystems to return probabilities for, - if None return for all subsystems (Default: None). + if None return for all subsystems (Default: None). decimals (None or int): the number of decimal places to round - values. If None no rounding is done (Default: None). + values. If None no rounding is done (Default: None) Returns: - dict: The measurement probabilities in dict (ket) form. + dict[str, float]: The measurement probabilities in dict (ket) form. """ - if qargs is None: - qubits = range(self.clifford.num_qubits) - else: - qubits = qargs + return self._get_probabilities_dict( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ) - outcome = ["X"] * len(qubits) - outcome_prob = 1.0 - probs = {} # probabilities dictionary + def probabilities_dict( + self, qargs: None | list = None, decimals: None | int = None + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary. - self._get_probabilities(qubits, outcome, outcome_prob, probs) + Measurement probabilities are with respect to measurement in the + computation (diagonal) basis. - if decimals is not None: - for key, value in probs.items(): - probs[key] = round(value, decimals) + This dictionary representation uses a Ket-like notation where the + dictionary keys are qudit strings for the subsystem basis vectors. + If any subsystem has a dimension greater than 10 comma delimiters are + inserted between integers so that subsystems can be distinguished. - return probs + Args: + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + return self._get_probabilities_dict(outcome_bitstring=None, qargs=qargs, decimals=decimals) def reset(self, qargs: list | None = None) -> StabilizerState: """Reset state or subsystems to the 0-state. @@ -644,22 +667,48 @@ def _rowsum_deterministic(clifford, aux_pauli, row): # ----------------------------------------------------------------------- # Helper functions for calculating the probabilities # ----------------------------------------------------------------------- - def _get_probabilities(self, qubits, outcome, outcome_prob, probs): - """Recursive helper function for calculating the probabilities""" + def _get_probabilities( + self, + qubits: range, + outcome: list[str], + outcome_prob: float, + probs: dict[str, float], + outcome_bitstring: str = None, + ): + """Recursive helper function for calculating the probabilities - qubit_for_branching = -1 - ret = self.copy() + Args: + qubits (range): range of qubits + outcome (list[str]): outcome being built + outcome_prob (float): probabilitiy of the outcome + probs (dict[str, float]): holds the outcomes and probabilitiy results + outcome_bitstring (str): target outcome to measure which reduces measurements, None + if not targeting a specific target + """ + qubit_for_branching: int = -1 + ret: StabilizerState = self.copy() + + # Find outcomes for each qubit for i in range(len(qubits)): - qubit = qubits[len(qubits) - i - 1] if outcome[i] == "X": - is_deterministic = not any(ret.clifford.stab_x[:, qubit]) - if is_deterministic: - single_qubit_outcome = ret._measure_and_update(qubit, 0) - if single_qubit_outcome: - outcome[i] = "1" + # Retrieve the qubit for the current measurement + qubit = qubits[(len(qubits) - i - 1)] + # Determine if the probabilitiy is deterministic + if not any(ret.clifford.stab_x[:, qubit]): + single_qubit_outcome: np.int64 = ret._measure_and_update(qubit, 0) + if outcome_bitstring is None or ( + int(outcome_bitstring[i]) == single_qubit_outcome + ): + # No outcome_bitstring target, or using outcome_bitstring target and + # the single_qubit_outcome equals the desired outcome_bitstring target value, + # then use current outcome_prob value + outcome[i] = str(single_qubit_outcome) else: - outcome[i] = "0" + # If the single_qubit_outcome does not equal the outcome_bitsring target + # then we know that the probability will be 0 + outcome[i] = str(outcome_bitstring[i]) + outcome_prob = 0 else: qubit_for_branching = i @@ -668,15 +717,57 @@ def _get_probabilities(self, qubits, outcome, outcome_prob, probs): probs[str_outcome] = outcome_prob return - for single_qubit_outcome in range(0, 2): + for single_qubit_outcome in ( + range(0, 2) + if (outcome_bitstring is None) + else [int(outcome_bitstring[qubit_for_branching])] + ): new_outcome = outcome.copy() - if single_qubit_outcome: - new_outcome[qubit_for_branching] = "1" - else: - new_outcome[qubit_for_branching] = "0" + new_outcome[qubit_for_branching] = str(single_qubit_outcome) stab_cpy = ret.copy() stab_cpy._measure_and_update( - qubits[len(qubits) - qubit_for_branching - 1], single_qubit_outcome + qubits[(len(qubits) - qubit_for_branching - 1)], single_qubit_outcome + ) + stab_cpy._get_probabilities( + qubits, new_outcome, (0.5 * outcome_prob), probs, outcome_bitstring ) - stab_cpy._get_probabilities(qubits, new_outcome, 0.5 * outcome_prob, probs) + + def _get_probabilities_dict( + self, + outcome_bitstring: None | str = None, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Helper Function for calculating the subsystem measurement probability dictionary. + When the targeted outcome_bitstring value is set, then only the single outcome_bitstring + probability will be calculated. + + Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + if qargs is None: + qubits = range(self.clifford.num_qubits) + else: + qubits = qargs + + outcome = ["X"] * len(qubits) + outcome_prob = 1.0 + probs: dict[str, float] = {} # Probabilities dict to return with the measured values + + self._get_probabilities(qubits, outcome, outcome_prob, probs, outcome_bitstring) + + if decimals is not None: + for key, value in probs.items(): + probs[key] = round(value, decimals) + + return probs diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml new file mode 100644 index 00000000000..73da8e6b7ad --- /dev/null +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + The :class:'.StabilizerState' class now has a new method + :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + user to pass single bitstring to measure an outcome for. Previouslly the + :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + at worst case calculate (2^n) number of probabilbity calculations (depending + on the state), even if a user wanted a single result. With this new method + the user can calculate just the single outcome bitstring value a user passes + to measure the probability for. As the number of qubits increases, the more + prevelant the performance enhancement may be (depending on the state) as only + 1 bitstring result is measured. diff --git a/test/python/quantum_info/states/test_stabilizerstate.py b/test/python/quantum_info/states/test_stabilizerstate.py index 56fecafbe58..4e1659ff699 100644 --- a/test/python/quantum_info/states/test_stabilizerstate.py +++ b/test/python/quantum_info/states/test_stabilizerstate.py @@ -13,6 +13,7 @@ """Tests for Stabilizerstate quantum state class.""" +from itertools import product import unittest import logging from ddt import ddt, data, unpack @@ -32,6 +33,61 @@ logger = logging.getLogger(__name__) +class StabilizerStateTestingTools: + """Test tools for verifying test cases in StabilizerState""" + + @staticmethod + def _bitstring_product_dict(bitstring_length: int, skip_entries: dict = None) -> dict: + """Retrieves a dict of every possible product of '0', '1' for length bitstring_length + pass in a dict to use the keys as entries to skip adding to the dict + + Args: + bitstring_length (int): length of the bitstring product + skip_entries (dict[str, float], optional): dict entries to skip adding to the dict based + on existing keys in the dict passed in. Defaults to {}. + + Returns: + dict[str, float]: dict with entries, all set to 0 + """ + if skip_entries is None: + skip_entries = {} + return { + result: 0 + for result in ["".join(x) for x in product(["0", "1"], repeat=bitstring_length)] + if result not in skip_entries + } + + @staticmethod + def _verify_individual_bitstrings( + testcase: QiskitTestCase, + target_dict: dict, + stab: StabilizerState, + qargs: list = None, + decimals: int = None, + dict_almost_equal: bool = False, + ) -> None: + """Helper that iterates through the target_dict and checks all probabilities by + running the value through the probabilities_dict_from_bitstring method for + retrieving a single measurement + + Args: + target_dict (dict[str, float]): dict to check probabilities for + stab (StabilizerState): stabilizerstate object to run probabilities_dict_from_bitstring on + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None) + dict_almost_equal (bool): utilize assertDictAlmostEqual when true, assertDictEqual when false + """ + for outcome_bitstring in target_dict: + (testcase.assertDictAlmostEqual if (dict_almost_equal) else testcase.assertDictEqual)( + stab.probabilities_dict_from_bitstring( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ), + {outcome_bitstring: target_dict[outcome_bitstring]}, + ) + + @ddt class TestStabilizerState(QiskitTestCase): """Tests for StabilizerState class.""" @@ -315,6 +371,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 1} self.assertEqual(value, target) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -326,6 +384,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"1": 1} self.assertEqual(value, target) + target.update({"0": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0, 1]) self.assertTrue(np.allclose(probs, target)) @@ -338,6 +398,7 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -355,43 +416,56 @@ def test_probabilities_dict_two_qubits(self): value = stab.probabilities_dict() target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0, 1] for _ in range(self.samples): with self.subTest(msg="P([0, 1])"): - value = stab.probabilities_dict([0, 1]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0, 1]) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1, 0] for _ in range(self.samples): with self.subTest(msg="P([1, 0])"): - value = stab.probabilities_dict([1, 0]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "10": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([1, 0]) + target.update({"01": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0.5, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0] for _ in range(self.samples): with self.subTest(msg="P[0]"): - value = stab.probabilities_dict([0]) + value = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0]) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1] for _ in range(self.samples): with self.subTest(msg="P([1])"): - value = stab.probabilities_dict([1]) + value = stab.probabilities_dict(qargs) target = {"0": 1.0} self.assertEqual(value, target) - probs = stab.probabilities([1]) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -405,9 +479,10 @@ def test_probabilities_dict_qubits(self): qc.h(2) stab = StabilizerState(qc) + decimals: int = 1 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=1"): - value = stab.probabilities_dict(decimals=1) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.1, "001": 0.1, @@ -419,13 +494,17 @@ def test_probabilities_dict_qubits(self): "111": 0.1, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=1) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 2 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=2"): - value = stab.probabilities_dict(decimals=2) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.12, "001": 0.12, @@ -437,13 +516,17 @@ def test_probabilities_dict_qubits(self): "111": 0.12, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=2) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 3 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=3"): - value = stab.probabilities_dict(decimals=3) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.125, "001": 0.125, @@ -455,10 +538,72 @@ def test_probabilities_dict_qubits(self): "111": 0.125, } self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) probs = stab.probabilities(decimals=3) target = np.array([0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]) self.assertTrue(np.allclose(probs, target)) + @combine(num_qubits=[5, 6, 7, 8, 9]) + def test_probabilities_dict_from_bitstring(self, num_qubits): + """Test probabilities_dict_from_bitstring methods with medium number of qubits that are still + reasonable to calculate the full dict with probabilities_dict of all possible outcomes""" + + qc: QuantumCircuit = QuantumCircuit(num_qubits) + for qubit_num in range(0, num_qubits): + qc.h(qubit_num) + stab = StabilizerState(qc) + + expected_result: float = float(1 / (2**num_qubits)) + target_dict: dict = StabilizerStateTestingTools._bitstring_product_dict(num_qubits) + target_dict.update((k, expected_result) for k in target_dict) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + value = stab.probabilities_dict() + self.assertDictEqual(value, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target_dict, stab) + probs = stab.probabilities() + target = np.array(([expected_result] * (2**num_qubits))) + self.assertTrue(np.allclose(probs, target)) + + # H gate at qubit 0, Every gate after is an X gate + # will result in 2 outcomes with 0.5 + qc = QuantumCircuit(num_qubits) + qc.h(0) + for qubit_num in range(1, num_qubits): + qc.x(qubit_num) + stab = StabilizerState(qc) + + # Build the 2 expected outcome bitstrings for + # 0.5 probability based on h and x gates + target_1: str = "".join(["1" * (num_qubits - 1)] + ["0"]) + target_2: str = "".join(["1" * num_qubits]) + target: dict = {target_1: 0.5, target_2: 0.5} + target_all_bitstrings: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, target + ) + target_all_bitstrings.update(target_all_bitstrings) + + # Numpy Array to verify stab.probabilities() + target_np_dict: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, [target_1, target_2] + ) + target_np_dict.update(target) + target_np_array: np.ndarray = np.array(list(target_np_dict.values())) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + stab = StabilizerState(qc) + value = stab.probabilities_dict() + self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_all_bitstrings, stab + ) + probs = stab.probabilities() + self.assertTrue(np.allclose(probs, target_np_array)) + def test_probabilities_dict_ghz(self): """Test probabilities and probabilities_dict method of a subsystem of qubits""" @@ -473,6 +618,8 @@ def test_probabilities_dict_ghz(self): value = stab.probabilities_dict() target = {"000": 0.5, "111": 0.5} self.assertEqual(value, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target)) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -483,6 +630,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"000": 0.5, "111": 0.5} self.assertDictAlmostEqual(probs, target) + target.update( + StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target) + ) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -493,6 +644,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"00": 0.5, "11": 0.5} self.assertDictAlmostEqual(probs, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(2, target)) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -503,6 +658,9 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertDictAlmostEqual(probs, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -520,10 +678,17 @@ def test_probs_random_subsystem(self, num_qubits): stab = StabilizerState(cliff) probs = stab.probabilities(qargs) probs_dict = stab.probabilities_dict(qargs) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, probs_dict, stab, qargs + ) target = Statevector(qc).probabilities(qargs) target_dict = Statevector(qc).probabilities_dict(qargs) + Statevector(qc).probabilities_dict() self.assertTrue(np.allclose(probs, target)) self.assertDictAlmostEqual(probs_dict, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_dict, stab, qargs, dict_almost_equal=True + ) @combine(num_qubits=[2, 3, 4, 5]) def test_expval_from_random_clifford(self, num_qubits): @@ -972,10 +1137,22 @@ def test_stabilizer_bell_equiv(self): # [XX, -ZZ] and [XX, YY] both generate the stabilizer group {II, XX, YY, -ZZ} self.assertTrue(cliff1.equiv(cliff2)) self.assertEqual(cliff1.probabilities_dict(), cliff2.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff1.probabilities_dict(), cliff2 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff2.probabilities_dict(), cliff1 + ) # [XX, ZZ] and [XX, -YY] both generate the stabilizer group {II, XX, -YY, ZZ} self.assertTrue(cliff3.equiv(cliff4)) self.assertEqual(cliff3.probabilities_dict(), cliff4.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff3.probabilities_dict(), cliff4 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff4.probabilities_dict(), cliff3 + ) self.assertFalse(cliff1.equiv(cliff3)) self.assertFalse(cliff2.equiv(cliff4)) From 70b36d061c5423dd0630c24df2a16be499a08c12 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 9 May 2024 12:51:58 -0400 Subject: [PATCH 051/159] improve docstring of plot_circuit_layout (#12370) * improve docstring of plot_circuit_layout * "name" -> "index" --- qiskit/visualization/gate_map.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index d8ffb6e1038..c7cc0727a6c 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -1122,7 +1122,13 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None Args: circuit (QuantumCircuit): Input quantum circuit. backend (Backend): Target backend. - view (str): Layout view: either 'virtual' or 'physical'. + view (str): How to label qubits in the layout. Options: + + - ``"virtual"``: Label each qubit with the index of the virtual qubit that + mapped to it. + - ``"physical"``: Label each qubit with the index of the physical qubit that it + corresponds to on the device. + qubit_coordinates (Sequence): An optional sequence input (list or array being the most common) of 2d coordinates for each qubit. The length of the sequence must match the number of qubits on the backend. The sequence From 24f1436fdb5d4061fde3a6fab6dd5139556f7e10 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 9 May 2024 14:19:07 -0400 Subject: [PATCH 052/159] fix edge coloring bug in plot_coupling_map (#12369) * fix edge coloring bug in plot circuit layout * add release note --- qiskit/visualization/gate_map.py | 2 ++ releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index c7cc0727a6c..b950c84c902 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -1039,7 +1039,9 @@ def plot_coupling_map( graph = CouplingMap(coupling_map).graph if not plot_directed: + line_color_map = dict(zip(graph.edge_list(), line_color)) graph = graph.to_undirected(multigraph=False) + line_color = [line_color_map[edge] for edge in graph.edge_list()] for node in graph.node_indices(): graph[node] = node diff --git a/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml new file mode 100644 index 00000000000..72f2c95962a --- /dev/null +++ b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :func:`plot_coupling_map` that caused the edges of the coupling map to be colored incorrectly. + See https://github.com/Qiskit/qiskit/pull/12369 for details. From 8c8c78a345caf35c85e30387b200565fd7f970de Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 9 May 2024 15:11:30 -0400 Subject: [PATCH 053/159] Reorganize API index page into sections (#12333) * Reorganize API index page into sections * Sort alphabetically within each section * Clarify ordering expectation --- docs/apidoc/index.rst | 85 +++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 3a6c1b04cfd..30bb20998d6 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -1,42 +1,89 @@ .. module:: qiskit +.. + Within each section, the modules should be ordered alphabetically by + module name (not RST filename). ============= API Reference ============= +Circuit construction: + .. toctree:: :maxdepth: 1 circuit - circuit_library circuit_classical - circuit_singleton - compiler - visualization classicalfunction + circuit_library + circuit_singleton + +Quantum information: + +.. toctree:: + :maxdepth: 1 + + quantum_info + +Transpilation: + +.. toctree:: + :maxdepth: 1 + converters - assembler dagcircuit passmanager + synthesis + qiskit.synthesis.unitary.aqc + transpiler + transpiler_passes + transpiler_synthesis_plugins + transpiler_preset + transpiler_plugins + +Primitives and providers: + +.. toctree:: + :maxdepth: 1 + + primitives providers providers_basic_provider providers_fake_provider providers_models - pulse - scheduler - synthesis - qiskit.synthesis.unitary.aqc - primitives + +Results and visualizations: + +.. toctree:: + :maxdepth: 1 + + result + visualization + +Serialization: + +.. toctree:: + :maxdepth: 1 + qasm2 qasm3 - qobj qpy - quantum_info - result - transpiler - transpiler_passes - transpiler_preset - transpiler_plugins - transpiler_synthesis_plugins - utils + +Pulse-level programming: + +.. toctree:: + :maxdepth: 1 + + pulse + scheduler + +Other: + +.. toctree:: + :maxdepth: 1 + + assembler + compiler exceptions + qobj + utils From 4a6c57044f35f4fab62b3f10764c6463a2b75f03 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 9 May 2024 15:31:06 -0400 Subject: [PATCH 054/159] Fix runtime scaling of StarPreRouting pass (#12376) This commit fixes a runtime performance scaling issue with the new StarPreRouting pass. If there are any stars identified by the pass when the pass goes to pre-route those star connectivity blocks it specifies a custom lexicographical topological sort key to ensure the stars are kept together in the sort. However the mechanism by which this sort key was generated scaled quadratically with the number of DAG nodes in the identified stars. This ended up being a large runtime performance bottleneck. This commit fixes this issue by pre-computing the sort key for all nodes in the stars and putting that in a dictionary so that when we call rustworkx to perform the topological sort the sort key callback does not become the bottleneck for the entire pass. --- qiskit/transpiler/passes/routing/star_prerouting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index 4b27749c6da..b79a298ad59 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -329,13 +329,13 @@ def _apply_mapping(qargs, qubit_mapping, qubits): last_2q_gate = None int_digits = floor(log10(len(processing_order))) + 1 - processing_order_s = set(processing_order) + processing_order_index_map = { + node: f"a{str(index).zfill(int(int_digits))}" + for index, node in enumerate(processing_order) + } def tie_breaker_key(node): - if node in processing_order_s: - return "a" + str(processing_order.index(node)).zfill(int(int_digits)) - else: - return node.sort_key + 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) From 48709afe0489febf965c237696e4f3376936287f Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Thu, 9 May 2024 15:59:11 -0400 Subject: [PATCH 055/159] Removing unnecessary-dict-index-lookup lint rule and updates (#12373) --- pyproject.toml | 1 - qiskit/transpiler/target.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c7094827c8..303192e75cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,7 +226,6 @@ disable = [ "no-value-for-parameter", "not-context-manager", "unexpected-keyword-arg", - "unnecessary-dict-index-lookup", "unnecessary-dunder-call", "unnecessary-lambda-assignment", "unspecified-encoding", diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 8d609ce3b8a..466d93fc89e 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -814,7 +814,7 @@ def check_obj_params(parameters, obj): if qargs in self._gate_map[op_name]: return True if self._gate_map[op_name] is None or None in self._gate_map[op_name]: - return self._gate_name_map[op_name].num_qubits == len(qargs) and all( + return obj.num_qubits == len(qargs) and all( x < self.num_qubits for x in qargs ) return False From 4ede4701d1b245f6ce35aa184a938e3be41711b2 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Thu, 9 May 2024 16:02:40 -0400 Subject: [PATCH 056/159] Removing consider-iterating-dictionary lint rule and updates (#12366) --- pyproject.toml | 1 - qiskit/circuit/library/n_local/pauli_two_design.py | 2 +- qiskit/pulse/parser.py | 4 ++-- .../synthesis/discrete_basis/generate_basis_approximations.py | 2 +- qiskit/synthesis/discrete_basis/solovay_kitaev.py | 2 +- qiskit/transpiler/passes/synthesis/plugin.py | 4 ++-- qiskit/visualization/circuit/matplotlib.py | 2 +- qiskit/visualization/circuit/qcstyle.py | 4 ++-- test/python/transpiler/test_high_level_synthesis.py | 4 ++-- 9 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 303192e75cf..45725f51bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,7 +218,6 @@ disable = [ # with the rationale "arguments-renamed", "broad-exception-raised", - "consider-iterating-dictionary", "consider-using-dict-items", "consider-using-enumerate", "consider-using-f-string", diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index b79f0888938..71b090d0884 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -118,7 +118,7 @@ def _build_rotation_layer(self, circuit, param_iter, i): qubits = range(self.num_qubits) # if no gates for this layer were generated, generate them - if i not in self._gates.keys(): + if i not in self._gates: self._gates[i] = list(self._rng.choice(["rx", "ry", "rz"], self.num_qubits)) # if not enough gates exist, add more elif len(self._gates[i]) < self.num_qubits: diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py index a9e752f562e..8e31faebf77 100644 --- a/qiskit/pulse/parser.py +++ b/qiskit/pulse/parser.py @@ -120,7 +120,7 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio if kwargs: for key, val in kwargs.items(): if key in self.params: - if key not in self._locals_dict.keys(): + if key not in self._locals_dict: self._locals_dict[key] = val else: raise PulseError( @@ -272,7 +272,7 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: node = copy.copy(node) node.args = [self.visit(arg) for arg in node.args] if all(isinstance(arg, ast.Constant) for arg in node.args): - if node.func.id not in self._math_ops.keys(): + if node.func.id not in self._math_ops: raise PulseError("Function %s is not supported." % node.func.id) _args = [arg.value for arg in node.args] _val = self._math_ops[node.func.id](*_args) diff --git a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py index 07139b223b1..672d0eb9e8e 100644 --- a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py +++ b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py @@ -137,7 +137,7 @@ def generate_basic_approximations( basis = [] for gate in basis_gates: if isinstance(gate, str): - if gate not in _1q_gates.keys(): + if gate not in _1q_gates: raise ValueError(f"Invalid gate identifier: {gate}") basis.append(gate) else: # gate is a qiskit.circuit.Gate diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 62ad50582d4..e1db47beaef 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -180,7 +180,7 @@ def _remove_inverse_follows_gate(sequence): while index < len(sequence.gates) - 1: curr_gate = sequence.gates[index] next_gate = sequence.gates[index + 1] - if curr_gate.name in _1q_inverses.keys(): + if curr_gate.name in _1q_inverses: remove = _1q_inverses[curr_gate.name] == next_gate.name else: remove = curr_gate.inverse() == next_gate diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index f2485bfee53..c57c6d76f9f 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -698,13 +698,13 @@ def __init__(self): self.plugins_by_op = {} for plugin_name in self.plugins.names(): op_name, method_name = plugin_name.split(".") - if op_name not in self.plugins_by_op.keys(): + if op_name not in self.plugins_by_op: self.plugins_by_op[op_name] = [] self.plugins_by_op[op_name].append(method_name) def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index c547846acc5..b4252065006 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -893,7 +893,7 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic this_clbit_dict = {} for clbit in clbits_dict.values(): y = clbit["y"] - fold_num * (glob_data["n_lines"] + 1) - if y not in this_clbit_dict.keys(): + if y not in this_clbit_dict: this_clbit_dict[y] = { "val": 1, "wire_label": clbit["wire_label"], diff --git a/qiskit/visualization/circuit/qcstyle.py b/qiskit/visualization/circuit/qcstyle.py index a8432ca86a9..67ae9faaf24 100644 --- a/qiskit/visualization/circuit/qcstyle.py +++ b/qiskit/visualization/circuit/qcstyle.py @@ -72,7 +72,7 @@ class StyleDict(dict): def __setitem__(self, key: Any, value: Any) -> None: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] if key not in self.VALID_FIELDS: @@ -85,7 +85,7 @@ def __setitem__(self, key: Any, value: Any) -> None: def __getitem__(self, key: Any) -> Any: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] return super().__getitem__(key) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 9a2432b82f9..f20b102d183 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -126,7 +126,7 @@ class OpARepeatSynthesisPlugin(HighLevelSynthesisPlugin): """The repeat synthesis for opA""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - if "n" not in options.keys(): + if "n" not in options: return None qc = QuantumCircuit(1) @@ -206,7 +206,7 @@ def __init__(self): def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] From b80885d1d617bd96ab5314db1b50d959258c2f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 9 May 2024 22:15:36 +0200 Subject: [PATCH 057/159] Fix missing layout in `Commuting2qGateRouter` (#12137) * Add layout to property set, add test * Apply comments from code review * Add virtual permutation instead of final layout. Test --- .../commuting_2q_gate_router.py | 9 +++- ...x-swap-router-layout-f28cf0a2de7976a8.yaml | 7 +++ .../transpiler/test_swap_strategy_router.py | 44 ++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py index 402aa9146f0..501400f70ce 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py @@ -160,8 +160,13 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if len(dag.qubits) != next(iter(dag.qregs.values())).size: raise TranspilerError("Circuit has qubits not contained in the qubit register.") - new_dag = dag.copy_empty_like() + # Fix output permutation -- copied from ElidePermutations + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + new_dag = dag.copy_empty_like() current_layout = Layout.generate_trivial_layout(*dag.qregs.values()) # Used to keep track of nodes that do not decompose using swap strategies. @@ -183,6 +188,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._compose_non_swap_nodes(accumulator, current_layout, new_dag) + self.property_set["virtual_permutation_layout"] = current_layout + return new_dag def _compose_non_swap_nodes( diff --git a/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml new file mode 100644 index 00000000000..834d7986ab8 --- /dev/null +++ b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an oversight in the :class:`.Commuting2qGateRouter` transpiler pass where the qreg permutations + were not added to the pass property set, so they would have to be tracked manually by the user. Now it's + possible to access the permutation through the output circuit's ``layout`` property and plug the pass + into any transpilation pipeline without loss of information. diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index 4a46efd57b2..d6ca1bde53d 100644 --- a/test/python/transpiler/test_swap_strategy_router.py +++ b/test/python/transpiler/test_swap_strategy_router.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 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 @@ -15,12 +15,14 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, Qubit, QuantumRegister +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import PassManager, CouplingMap, Layout, TranspilerError from qiskit.circuit.library import PauliEvolutionGate, CXGate from qiskit.circuit.library.n_local import QAOAAnsatz from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.passes import FullAncillaAllocation from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import ApplyLayout @@ -562,9 +564,47 @@ def test_edge_coloring(self, edge_coloring): self.assertEqual(pm_.run(circ), expected) + def test_permutation_tracking(self): + """Test that circuit layout permutations are properly tracked in the pass property + set and returned with the output circuit.""" + + # We use the same scenario as the QAOA test above + mixer = QuantumCircuit(4) + for idx in range(4): + mixer.ry(-idx, idx) + + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + circ = QAOAAnsatz(op, reps=2, mixer_operator=mixer) + + expected_swap_permutation = [3, 1, 2, 0] + expected_full_permutation = [1, 3, 2, 0] + + cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) + swap_strat = SwapStrategy(cmap, swap_layers=[[(0, 1), (2, 3)], [(1, 2)]]) + + # test standalone + swap_pm = PassManager( + [ + FindCommutingPauliEvolutions(), + Commuting2qGateRouter(swap_strat), + ] + ) + swapped = swap_pm.run(circ.decompose()) + + # test as pre-routing step + backend = GenericBackendV2(num_qubits=4, coupling_map=[[0, 1], [0, 2], [0, 3]], seed=42) + pm = generate_preset_pass_manager( + optimization_level=3, target=backend.target, seed_transpiler=40 + ) + pm.pre_routing = swap_pm + full = pm.run(circ.decompose()) + + self.assertEqual(swapped.layout.routing_permutation(), expected_swap_permutation) + self.assertEqual(full.layout.routing_permutation(), expected_full_permutation) + class TestSwapRouterExceptions(QiskitTestCase): - """Test that exceptions are properly raises.""" + """Test that exceptions are properly raised.""" def setUp(self): """Setup useful variables.""" From 235e581b1f76f29add0399989ed47f47a4e98bb8 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Fri, 10 May 2024 06:19:31 -0400 Subject: [PATCH 058/159] Removing consider-using-dict-items from lint exclusions and updates (#12288) * removing consider-using-dict-items from lint exclusions and updates * switching items to values * latex initialization updates for readability * quick update for using items after removing lint rule * Github to GitHub in SECURITY.md --- SECURITY.md | 4 ++-- pyproject.toml | 1 - qiskit/dagcircuit/collect_blocks.py | 4 ++-- .../basic_provider/basic_simulator.py | 4 ++-- .../providers/models/backendconfiguration.py | 4 ++-- qiskit/providers/options.py | 16 ++++++------- qiskit/pulse/library/symbolic_pulses.py | 4 ++-- qiskit/result/models.py | 8 +++---- qiskit/result/result.py | 8 +++---- .../optimization/collect_multiqubit_blocks.py | 4 ++-- qiskit/transpiler/target.py | 6 +++-- qiskit/visualization/circuit/latex.py | 23 +++++++++++-------- qiskit/visualization/circuit/text.py | 3 +-- .../primitives/test_backend_sampler_v2.py | 4 ++-- .../primitives/test_statevector_sampler.py | 4 ++-- 15 files changed, 51 insertions(+), 46 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1a8e9cea671..45e6a9d6f51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,13 +15,13 @@ We provide more detail on [the release and support schedule of Qiskit in our doc ## Reporting a Vulnerability To report vulnerabilities, you can privately report a potential security issue -via the Github security vulnerabilities feature. This can be done here: +via the GitHub security vulnerabilities feature. This can be done here: https://github.com/Qiskit/qiskit/security/advisories Please do **not** open a public issue about a potential security vulnerability. -You can find more details on the security vulnerability feature in the Github +You can find more details on the security vulnerability feature in the GitHub documentation here: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability diff --git a/pyproject.toml b/pyproject.toml index 45725f51bb4..9a12ac25fd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,7 +218,6 @@ disable = [ # with the rationale "arguments-renamed", "broad-exception-raised", - "consider-using-dict-items", "consider-using-enumerate", "consider-using-f-string", "no-member", diff --git a/qiskit/dagcircuit/collect_blocks.py b/qiskit/dagcircuit/collect_blocks.py index ea574536f45..c5c7b49144f 100644 --- a/qiskit/dagcircuit/collect_blocks.py +++ b/qiskit/dagcircuit/collect_blocks.py @@ -288,8 +288,8 @@ def run(self, block): self.group[self.find_leader(first)].append(node) blocks = [] - for index in self.leader: - if self.leader[index] == index: + for index, item in self.leader.items(): + if index == item: blocks.append(self.group[index]) return blocks diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index b03a8df7ae5..978e1dad56f 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -528,13 +528,13 @@ def run( from qiskit.compiler import assemble out_options = {} - for key in backend_options: + for key, value in backend_options.items(): if not hasattr(self.options, key): warnings.warn( "Option %s is not used by this backend" % key, UserWarning, stacklevel=2 ) else: - out_options[key] = backend_options[key] + out_options[key] = value qobj = assemble(run_input, self, **out_options) qobj_options = qobj.config self._set_options(qobj_config=qobj_options, backend_options=backend_options) diff --git a/qiskit/providers/models/backendconfiguration.py b/qiskit/providers/models/backendconfiguration.py index e346e293a38..ebd0a6d9bbb 100644 --- a/qiskit/providers/models/backendconfiguration.py +++ b/qiskit/providers/models/backendconfiguration.py @@ -892,9 +892,9 @@ def get_qubit_channels(self, qubit: Union[int, Iterable[int]]) -> List[Channel]: channels = set() try: if isinstance(qubit, int): - for key in self._qubit_channel_map.keys(): + for key, value in self._qubit_channel_map.items(): if qubit in key: - channels.update(self._qubit_channel_map[key]) + channels.update(value) if len(channels) == 0: raise KeyError elif isinstance(qubit, list): diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index 7a5b7a26035..e96ffc57842 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -233,24 +233,24 @@ def set_validator(self, field, validator_value): def update_options(self, **fields): """Update options with kwargs""" - for field in fields: - field_validator = self.validator.get(field, None) + for field_name, field in fields.items(): + field_validator = self.validator.get(field_name, None) if isinstance(field_validator, tuple): - if fields[field] > field_validator[1] or fields[field] < field_validator[0]: + if field > field_validator[1] or field < field_validator[0]: raise ValueError( - f"Specified value for '{field}' is not a valid value, " + f"Specified value for '{field_name}' is not a valid value, " f"must be >={field_validator[0]} or <={field_validator[1]}" ) elif isinstance(field_validator, list): - if fields[field] not in field_validator: + if field not in field_validator: raise ValueError( - f"Specified value for {field} is not a valid choice, " + f"Specified value for {field_name} is not a valid choice, " f"must be one of {field_validator}" ) elif isinstance(field_validator, type): - if not isinstance(fields[field], field_validator): + if not isinstance(field, field_validator): raise TypeError( - f"Specified value for {field} is not of required type {field_validator}" + f"Specified value for {field_name} is not of required type {field_validator}" ) self._fields.update(fields) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9041dbc1951..b076bcf56cb 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -677,8 +677,8 @@ def __eq__(self, other: object) -> bool: if not np.isclose(complex_amp1, complex_amp2): return False - for key in self.parameters: - if key not in ["amp", "angle"] and self.parameters[key] != other.parameters[key]: + for key, value in self.parameters.items(): + if key not in ["amp", "angle"] and value != other.parameters[key]: return False return True diff --git a/qiskit/result/models.py b/qiskit/result/models.py index 07286148f88..99281019671 100644 --- a/qiskit/result/models.py +++ b/qiskit/result/models.py @@ -171,11 +171,11 @@ def __repr__(self): out += ", seed=%s" % self.seed if hasattr(self, "meas_return"): out += ", meas_return=%s" % self.meas_return - for key in self._metadata: - if isinstance(self._metadata[key], str): - value_str = "'%s'" % self._metadata[key] + for key, value in self._metadata.items(): + if isinstance(value, str): + value_str = "'%s'" % value else: - value_str = repr(self._metadata[key]) + value_str = repr(value) out += f", {key}={value_str}" out += ")" return out diff --git a/qiskit/result/result.py b/qiskit/result/result.py index d99be996080..c1792de56ae 100644 --- a/qiskit/result/result.py +++ b/qiskit/result/result.py @@ -81,11 +81,11 @@ def __repr__(self): ) ) out += f", date={self.date}, status={self.status}, header={self.header}" - for key in self._metadata: - if isinstance(self._metadata[key], str): - value_str = "'%s'" % self._metadata[key] + for key, value in self._metadata.items(): + if isinstance(value, str): + value_str = "'%s'" % value else: - value_str = repr(self._metadata[key]) + value_str = repr(value) out += f", {key}={value_str}" out += ")" return out diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index 51b39d7e961..e0dd61ff6cf 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -218,8 +218,8 @@ def collect_key(x): prev = bit self.gate_groups[self.find_set(prev)].append(nd) # need to turn all groups that still exist into their own blocks - for index in self.parent: - if self.parent[index] == index and len(self.gate_groups[index]) != 0: + for index, item in self.parent.items(): + if item == index and len(self.gate_groups[index]) != 0: block_list.append(self.gate_groups[index][:]) self.property_set["block_list"] = block_list diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 466d93fc89e..5af9e868658 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -940,7 +940,9 @@ def instructions(self): is globally defined. """ return [ - (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op] + (self._gate_name_map[op], qarg) + for op, qargs in self._gate_map.items() + for qarg in qargs ] def instruction_properties(self, index): @@ -979,7 +981,7 @@ def instruction_properties(self, index): InstructionProperties: The instruction properties for the specified instruction tuple """ instruction_properties = [ - inst_props for op in self._gate_map for _, inst_props in self._gate_map[op].items() + inst_props for qargs in self._gate_map.values() for inst_props in qargs.values() ] return instruction_properties[index] diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index ad4b8e070e1..9341126bcd1 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -213,17 +213,22 @@ def _initialize_latex_array(self): self._latex.append([" "] * (self._img_depth + 1)) # display the bit/register labels - for wire in self._wire_map: + for wire, index in self._wire_map.items(): if isinstance(wire, ClassicalRegister): register = wire - index = self._wire_map[wire] + wire_label = get_wire_label( + "latex", register, index, layout=self._layout, cregbundle=self._cregbundle + ) else: register, bit_index, reg_index = get_bit_reg_index(self._circuit, wire) - index = bit_index if register is None else reg_index + wire_label = get_wire_label( + "latex", + register, + bit_index if register is None else reg_index, + layout=self._layout, + cregbundle=self._cregbundle, + ) - wire_label = get_wire_label( - "latex", register, index, layout=self._layout, cregbundle=self._cregbundle - ) wire_label += " : " if self._initial_state: wire_label += "\\ket{{0}}" if isinstance(wire, Qubit) else "0" @@ -234,7 +239,7 @@ def _initialize_latex_array(self): self._latex[pos][1] = "\\lstick{/_{_{" + str(register.size) + "}}} \\cw" wire_label = f"\\mathrm{{{wire_label}}}" else: - pos = self._wire_map[wire] + pos = index self._latex[pos][0] = "\\nghost{" + wire_label + " & " + "\\lstick{" + wire_label def _get_image_depth(self): @@ -620,11 +625,11 @@ def _add_condition(self, op, wire_list, col): # First sort the val_bits in the order of the register bits in the circuit cond_wires = [] cond_bits = [] - for wire in self._wire_map: + for wire, index in self._wire_map.items(): reg, _, reg_index = get_bit_reg_index(self._circuit, wire) if reg == cond_reg: cond_bits.append(reg_index) - cond_wires.append(self._wire_map[wire]) + cond_wires.append(index) gap = cond_wires[0] - max(wire_list) prev_wire = cond_wires[0] diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index abefe551177..c2846da0b75 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -894,10 +894,9 @@ def wire_names(self, with_initial_state=False): self._wire_map = get_wire_map(self._circuit, (self.qubits + self.clbits), self.cregbundle) wire_labels = [] - for wire in self._wire_map: + for wire, index in self._wire_map.items(): if isinstance(wire, ClassicalRegister): register = wire - index = self._wire_map[wire] else: register, bit_index, reg_index = get_bit_reg_index(self._circuit, wire) index = bit_index if register is None else reg_index diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 9f6c007b1d5..b03818846c8 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -640,9 +640,9 @@ def test_circuit_with_aliased_cregs(self, backend): self.assertEqual(len(result), 1) data = result[0].data self.assertEqual(len(data), 3) - for creg_name in target: + for creg_name, creg in target.items(): self.assertTrue(hasattr(data, creg_name)) - self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) + self._assert_allclose(getattr(data, creg_name), np.array(creg)) @combine(backend=BACKENDS) def test_no_cregs(self, backend): diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index de17b282407..c065871025d 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -606,9 +606,9 @@ def test_circuit_with_aliased_cregs(self): self.assertEqual(len(result), 1) data = result[0].data self.assertEqual(len(data), 3) - for creg_name in target: + for creg_name, creg in target.items(): self.assertTrue(hasattr(data, creg_name)) - self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) + self._assert_allclose(getattr(data, creg_name), np.array(creg)) def test_no_cregs(self): """Test that the sampler works when there are no classical register in the circuit.""" From 5fd7d01c447325b09bd236856c1c883f413dc08d Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Fri, 10 May 2024 06:30:38 -0400 Subject: [PATCH 059/159] Moving the unsupported-assignment-operation lint rule (#12304) * Moving the unsupported-assignment-operation lint rule * moving lint exclusion to inline comment --- pyproject.toml | 1 - qiskit/providers/options.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a12ac25fd7..1fde1d9991e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,7 +227,6 @@ disable = [ "unnecessary-dunder-call", "unnecessary-lambda-assignment", "unspecified-encoding", - "unsupported-assignment-operation", ] enable = [ diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index e96ffc57842..659af518a2b 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -229,7 +229,7 @@ def set_validator(self, field, validator_value): f"{type(validator_value)} is not a valid validator type, it " "must be a tuple, list, or class/type" ) - self.validator[field] = validator_value + self.validator[field] = validator_value # pylint: disable=unsupported-assignment-operation def update_options(self, **fields): """Update options with kwargs""" From fe275a0f15132073c6b5afb0c4be8bcc351479cb Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Fri, 10 May 2024 15:34:16 +0400 Subject: [PATCH 060/159] Fix {Pauli,SparsePauliOp}.apply_layout to raise an error with negative or duplicate indices (#12385) * fix apply_layout to raise an error with negative or duplicate indices * reno * Fix Sphinx syntax * Fix my own Sphinx lookup problem --------- Co-authored-by: Jake Lishman --- qiskit/quantum_info/operators/symplectic/pauli.py | 7 +++++-- .../operators/symplectic/sparse_pauli_op.py | 8 +++++--- ...-duplicate-negative-indices-cf5517921fe52706.yaml | 6 ++++++ .../quantum_info/operators/symplectic/test_pauli.py | 12 ++++++++++++ .../operators/symplectic/test_sparse_pauli_op.py | 12 ++++++++++++ 5 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index 1ccecc04a6c..8187c1ee41e 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -736,8 +736,11 @@ def apply_layout( n_qubits = num_qubits if layout is None: layout = list(range(self.num_qubits)) - elif any(x >= n_qubits for x in layout): - raise QiskitError("Provided layout contains indices outside the number of qubits.") + else: + if any(x < 0 or x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indices outside the number of qubits.") + if len(set(layout)) != len(layout): + raise QiskitError("Provided layout contains duplicate indices.") new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index dffe5b2396b..cf51579bef8 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -1139,7 +1139,6 @@ def apply_layout( specified will be applied without any expansion. If layout is None, the operator will be expanded to the given number of qubits. - Returns: A new :class:`.SparsePauliOp` with the provided layout applied """ @@ -1159,10 +1158,13 @@ def apply_layout( f"applied to a {n_qubits} qubit operator" ) n_qubits = num_qubits - if layout is not None and any(x >= n_qubits for x in layout): - raise QiskitError("Provided layout contains indices outside the number of qubits.") if layout is None: layout = list(range(self.num_qubits)) + else: + if any(x < 0 or x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indices outside the number of qubits.") + if len(set(layout)) != len(layout): + raise QiskitError("Provided layout contains duplicate indices.") new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml b/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml new file mode 100644 index 00000000000..9fbe0ffd9c7 --- /dev/null +++ b/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed :meth:`.SparsePauliOp.apply_layout` and :meth:`.Pauli.apply_layout` + to raise :exc:`.QiskitError` if duplicate indices or negative indices are provided + as part of a layout. diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py index 875dd923781..89324e8212e 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli.py @@ -606,6 +606,18 @@ def test_apply_layout_null_layout_invalid_num_qubits(self): with self.assertRaises(QiskitError): op.apply_layout(layout=None, num_qubits=1) + def test_apply_layout_negative_indices(self): + """Test apply_layout with negative indices""" + op = Pauli("IZ") + with self.assertRaises(QiskitError): + op.apply_layout(layout=[-1, 0], num_qubits=3) + + def test_apply_layout_duplicate_indices(self): + """Test apply_layout with duplicate indices""" + op = Pauli("IZ") + with self.assertRaises(QiskitError): + op.apply_layout(layout=[0, 0], num_qubits=3) + if __name__ == "__main__": unittest.main() diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 330fd53bc35..1149ef1f346 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -1179,6 +1179,18 @@ def test_apply_layout_null_layout_invalid_num_qubits(self): with self.assertRaises(QiskitError): op.apply_layout(layout=None, num_qubits=1) + def test_apply_layout_negative_indices(self): + """Test apply_layout with negative indices""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + with self.assertRaises(QiskitError): + op.apply_layout(layout=[-1, 0], num_qubits=3) + + def test_apply_layout_duplicate_indices(self): + """Test apply_layout with duplicate indices""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + with self.assertRaises(QiskitError): + op.apply_layout(layout=[0, 0], num_qubits=3) + if __name__ == "__main__": unittest.main() From f20dad0d03281bf3e628a7c32c117f41140365d4 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Fri, 10 May 2024 14:43:25 -0400 Subject: [PATCH 061/159] Removing broad-exception-raised lint rule and updates (#12356) * Removing broad-exception-raised lint rule and updates * updating assertion types based on review * updating tests based on review --- pyproject.toml | 1 - qiskit/quantum_info/quaternion.py | 2 +- qiskit/synthesis/clifford/clifford_decompose_bm.py | 2 +- qiskit/visualization/bloch.py | 4 ++-- qiskit/visualization/transition_visualization.py | 4 ++-- test/benchmarks/utils.py | 2 +- test/python/quantum_info/test_quaternions.py | 6 ++++-- test/python/transpiler/test_preset_passmanagers.py | 2 +- test/python/transpiler/test_sabre_swap.py | 2 +- test/python/transpiler/test_stochastic_swap.py | 2 +- 10 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1fde1d9991e..35a14a5524b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,7 +217,6 @@ disable = [ # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale "arguments-renamed", - "broad-exception-raised", "consider-using-enumerate", "consider-using-f-string", "no-member", diff --git a/qiskit/quantum_info/quaternion.py b/qiskit/quantum_info/quaternion.py index 22508b5ceb1..69b9b61c8f9 100644 --- a/qiskit/quantum_info/quaternion.py +++ b/qiskit/quantum_info/quaternion.py @@ -43,7 +43,7 @@ def __mul__(self, r): out_data[3] = r(0) * q(3) - r(1) * q(2) + r(2) * q(1) + r(3) * q(0) return Quaternion(out_data) else: - raise Exception("Multiplication by other not supported.") + return NotImplemented def norm(self): """Norm of quaternion.""" diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index 50ffcb74316..4800890a90d 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -192,7 +192,7 @@ def _cx_cost(clifford): return _cx_cost2(clifford) if clifford.num_qubits == 3: return _cx_cost3(clifford) - raise Exception("No Clifford CX cost function for num_qubits > 3.") + raise RuntimeError("No Clifford CX cost function for num_qubits > 3.") def _rank2(a, b, c, d): diff --git a/qiskit/visualization/bloch.py b/qiskit/visualization/bloch.py index bae0633a811..513d2ddff85 100644 --- a/qiskit/visualization/bloch.py +++ b/qiskit/visualization/bloch.py @@ -290,7 +290,7 @@ def set_label_convention(self, convention): self.zlabel = ["$\\circlearrowleft$", "$\\circlearrowright$"] self.xlabel = ["$\\leftrightarrow$", "$\\updownarrow$"] else: - raise Exception("No such convention.") + raise ValueError("No such convention.") def __str__(self): string = "" @@ -396,7 +396,7 @@ def add_annotation(self, state_or_vector, text, **kwargs): if isinstance(state_or_vector, (list, np.ndarray, tuple)) and len(state_or_vector) == 3: vec = state_or_vector else: - raise Exception("Position needs to be specified by a qubit " + "state or a 3D vector.") + raise TypeError("Position needs to be specified by a qubit state or a 3D vector.") self.annotations.append({"position": vec, "text": text, "opts": kwargs}) def make_sphere(self): diff --git a/qiskit/visualization/transition_visualization.py b/qiskit/visualization/transition_visualization.py index f322be64a4f..a2ff7479999 100644 --- a/qiskit/visualization/transition_visualization.py +++ b/qiskit/visualization/transition_visualization.py @@ -72,10 +72,10 @@ def __mul__(self, b): return self._multiply_with_quaternion(b) elif isinstance(b, (list, tuple, np.ndarray)): if len(b) != 3: - raise Exception(f"Input vector has invalid length {len(b)}") + raise ValueError(f"Input vector has invalid length {len(b)}") return self._multiply_with_vector(b) else: - raise Exception(f"Multiplication with unknown type {type(b)}") + return NotImplemented def _multiply_with_quaternion(self, q_2): """Multiplication of quaternion with quaternion""" diff --git a/test/benchmarks/utils.py b/test/benchmarks/utils.py index af8f3318074..d932e8d6a0c 100644 --- a/test/benchmarks/utils.py +++ b/test/benchmarks/utils.py @@ -71,7 +71,7 @@ def random_circuit( Exception: when invalid options given """ if max_operands < 1 or max_operands > 3: - raise Exception("max_operands must be between 1 and 3") + raise ValueError("max_operands must be between 1 and 3") one_q_ops = [ IGate, diff --git a/test/python/quantum_info/test_quaternions.py b/test/python/quantum_info/test_quaternions.py index 48e2ead8b89..d838ee2d1b5 100644 --- a/test/python/quantum_info/test_quaternions.py +++ b/test/python/quantum_info/test_quaternions.py @@ -92,12 +92,14 @@ def test_mul_by_quat(self): def test_mul_by_array(self): """Quaternions cannot be multiplied with an array.""" other_array = np.array([0.1, 0.2, 0.3, 0.4]) - self.assertRaises(Exception, self.quat_unnormalized.__mul__, other_array) + with self.assertRaises(TypeError): + _ = self.quat_unnormalized * other_array def test_mul_by_scalar(self): """Quaternions cannot be multiplied with a scalar.""" other_scalar = 0.123456789 - self.assertRaises(Exception, self.quat_unnormalized.__mul__, other_scalar) + with self.assertRaises(TypeError): + _ = self.quat_unnormalized * other_scalar def test_rotation(self): """Multiplication by -1 should give the same rotation.""" diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 247aa82ec03..c00208a2ffa 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -71,7 +71,7 @@ def mock_get_passmanager_stage( elif stage_name == "layout": return PassManager([]) else: - raise Exception("Failure, unexpected stage plugin combo for test") + raise RuntimeError("Failure, unexpected stage plugin combo for test") def emptycircuit(): diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index a9fec85be66..5315c4b8e01 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -1346,7 +1346,7 @@ def _visit_block(circuit, qubit_mapping=None): qargs = tuple(qubit_mapping[x] for x in instruction.qubits) if not isinstance(instruction.operation, ControlFlowOp): if len(qargs) > 2 or len(qargs) < 0: - raise Exception("Invalid number of qargs for instruction") + raise RuntimeError("Invalid number of qargs for instruction") if len(qargs) == 2: self.assertIn(qargs, self.coupling_edge_set) else: diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index df5948ed715..5b924e590a5 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -1506,7 +1506,7 @@ def _visit_block(circuit, qubit_mapping=None): qargs = tuple(qubit_mapping[x] for x in instruction.qubits) if not isinstance(instruction.operation, ControlFlowOp): if len(qargs) > 2 or len(qargs) < 0: - raise Exception("Invalid number of qargs for instruction") + raise RuntimeError("Invalid number of qargs for instruction") if len(qargs) == 2: self.assertIn(qargs, self.coupling_edge_set) else: From 8985b24c6b8f1886a681731cd8bf74b8202bfff7 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 14 May 2024 05:41:49 -0400 Subject: [PATCH 062/159] [DAGCircuit Oxidation] Port `DAGNode` to Rust (#12380) * Checkpoint before rebase. * [Tests passing] Port DAGNode classes to Rust. Also has beginnings of DAGCircuit, but this is not complete or wired up at all. * Revert DAGCircuit stuff. * Fix lint. * Address review comments. * Python lint + format * Fix format * Remove unnecessary state methods from CircuitInstruction pickling. --- crates/circuit/src/circuit_instruction.rs | 18 +- crates/circuit/src/dag_node.rs | 283 ++++++++++++++++++++++ crates/circuit/src/lib.rs | 5 + qiskit/dagcircuit/dagnode.py | 223 +++++------------ 4 files changed, 355 insertions(+), 174 deletions(-) create mode 100644 crates/circuit/src/dag_node.rs diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 48b0c4d20ee..86bd2e69c11 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -129,23 +129,7 @@ impl CircuitInstruction { ) } - fn __getstate__(&self, py: Python<'_>) -> PyObject { - ( - self.operation.bind(py), - self.qubits.bind(py), - self.clbits.bind(py), - ) - .into_py(py) - } - - fn __setstate__(&mut self, _py: Python<'_>, state: &Bound) -> PyResult<()> { - self.operation = state.get_item(0)?.extract()?; - self.qubits = state.get_item(1)?.extract()?; - self.clbits = state.get_item(2)?.extract()?; - Ok(()) - } - - pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + fn __getnewargs__(&self, py: Python<'_>) -> PyResult { Ok(( self.operation.bind(py), self.qubits.bind(py), diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs new file mode 100644 index 00000000000..c766461bb51 --- /dev/null +++ b/crates/circuit/src/dag_node.rs @@ -0,0 +1,283 @@ +// 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 crate::circuit_instruction::CircuitInstruction; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; +use pyo3::{intern, PyObject, PyResult}; + +/// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. +#[pyclass(module = "qiskit._accelerate.circuit", subclass)] +#[derive(Clone, Debug)] +pub struct DAGNode { + #[pyo3(get, set)] + pub _node_id: isize, +} + +#[pymethods] +impl DAGNode { + #[new] + #[pyo3(signature=(nid=-1))] + fn new(nid: isize) -> Self { + DAGNode { _node_id: nid } + } + + fn __getstate__(&self) -> isize { + self._node_id + } + + fn __setstate__(&mut self, nid: isize) { + self._node_id = nid; + } + + fn __lt__(&self, other: &DAGNode) -> bool { + self._node_id < other._node_id + } + + fn __gt__(&self, other: &DAGNode) -> bool { + self._node_id > other._node_id + } + + fn __str__(_self: &Bound) -> String { + format!("{}", _self.as_ptr() as usize) + } + + fn __hash__(&self, py: Python) -> PyResult { + self._node_id.into_py(py).bind(py).hash() + } +} + +/// Object to represent an Instruction at a node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGOpNode { + pub instruction: CircuitInstruction, + #[pyo3(get)] + pub sort_key: PyObject, +} + +#[pymethods] +impl DAGOpNode { + #[new] + fn new( + py: Python, + op: PyObject, + qargs: Option<&Bound>, + cargs: Option<&Bound>, + dag: Option<&Bound>, + ) -> PyResult<(Self, DAGNode)> { + let qargs = + qargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; + let cargs = + cargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; + + let sort_key = match dag { + Some(dag) => { + let cache = dag + .getattr(intern!(py, "_key_cache"))? + .downcast_into_exact::()?; + let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); + match cache.get_item(&cache_key)? { + Some(key) => key, + None => { + let indices: PyResult> = qargs + .iter() + .chain(cargs.iter()) + .map(|bit| { + dag.call_method1(intern!(py, "find_bit"), (bit,))? + .getattr(intern!(py, "index")) + }) + .collect(); + let index_strs: Vec<_> = + indices?.into_iter().map(|i| format!("{:04}", i)).collect(); + let key = PyString::new_bound(py, index_strs.join(",").as_str()); + cache.set_item(&cache_key, &key)?; + key.into_any() + } + } + } + None => qargs.str()?.into_any(), + }; + + Ok(( + DAGOpNode { + instruction: CircuitInstruction { + operation: op, + qubits: qargs.unbind(), + clbits: cargs.unbind(), + }, + sort_key: sort_key.unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyObject { + let state = (slf.as_ref()._node_id, &slf.sort_key); + ( + py.get_type_bound::(), + ( + &slf.instruction.operation, + &slf.instruction.qubits, + &slf.instruction.clbits, + ), + state, + ) + .into_py(py) + } + + fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { + let (nid, sort_key): (isize, PyObject) = state.extract()?; + slf.as_mut()._node_id = nid; + slf.sort_key = sort_key; + Ok(()) + } + + #[getter] + fn get_op(&self, py: Python) -> PyObject { + self.instruction.operation.clone_ref(py) + } + + #[setter] + fn set_op(&mut self, op: PyObject) { + self.instruction.operation = op; + } + + #[getter] + fn get_qargs(&self, py: Python) -> Py { + self.instruction.qubits.clone_ref(py) + } + + #[setter] + fn set_qargs(&mut self, qargs: Py) { + self.instruction.qubits = qargs; + } + + #[getter] + fn get_cargs(&self, py: Python) -> Py { + self.instruction.clbits.clone_ref(py) + } + + #[setter] + fn set_cargs(&mut self, cargs: Py) { + self.instruction.clbits = cargs; + } + + /// Returns the Instruction name corresponding to the op for this node + #[getter] + fn get_name(&self, py: Python) -> PyResult { + Ok(self + .instruction + .operation + .bind(py) + .getattr(intern!(py, "name"))? + .unbind()) + } + + /// Sets the Instruction name corresponding to the op for this node + #[setter] + fn set_name(&self, py: Python, new_name: PyObject) -> PyResult<()> { + self.instruction + .operation + .bind(py) + .setattr(intern!(py, "name"), new_name) + } + + /// Returns a representation of the DAGOpNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!( + "DAGOpNode(op={}, qargs={}, cargs={})", + self.instruction.operation.bind(py).repr()?, + self.instruction.qubits.bind(py).repr()?, + self.instruction.clbits.bind(py).repr()? + )) + } +} + +/// Object to represent an incoming wire node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGInNode { + #[pyo3(get)] + wire: PyObject, + #[pyo3(get)] + sort_key: PyObject, +} + +#[pymethods] +impl DAGInNode { + #[new] + fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok(( + DAGInNode { + wire, + sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyObject { + let state = (slf.as_ref()._node_id, &slf.sort_key); + (py.get_type_bound::(), (&slf.wire,), state).into_py(py) + } + + fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { + let (nid, sort_key): (isize, PyObject) = state.extract()?; + slf.as_mut()._node_id = nid; + slf.sort_key = sort_key; + Ok(()) + } + + /// Returns a representation of the DAGInNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!("DAGInNode(wire={})", self.wire.bind(py).repr()?)) + } +} + +/// Object to represent an outgoing wire node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGOutNode { + #[pyo3(get)] + wire: PyObject, + #[pyo3(get)] + sort_key: PyObject, +} + +#[pymethods] +impl DAGOutNode { + #[new] + fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok(( + DAGOutNode { + wire, + sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyObject { + let state = (slf.as_ref()._node_id, &slf.sort_key); + (py.get_type_bound::(), (&slf.wire,), state).into_py(py) + } + + fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { + let (nid, sort_key): (isize, PyObject) = state.extract()?; + slf.as_mut()._node_id = nid; + slf.sort_key = sort_key; + Ok(()) + } + + /// Returns a representation of the DAGOutNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!("DAGOutNode(wire={})", self.wire.bind(py).repr()?)) + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index cd560bad738..c186c4243e9 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -12,6 +12,7 @@ pub mod circuit_data; pub mod circuit_instruction; +pub mod dag_node; pub mod intern_context; use pyo3::prelude::*; @@ -30,6 +31,10 @@ pub enum SliceOrInt<'a> { #[pymodule] pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 97283c75b8f..9f35f6eda89 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -14,14 +14,11 @@ """Objects to represent the information at a node in the DAGCircuit.""" from __future__ import annotations -import itertools import typing import uuid -from collections.abc import Iterable - +import qiskit._accelerate.circuit from qiskit.circuit import ( - Qubit, Clbit, ClassicalRegister, ControlFlowOp, @@ -30,7 +27,6 @@ SwitchCaseOp, ForLoopOp, Parameter, - Operation, QuantumCircuit, ) from qiskit.circuit.classical import expr @@ -39,6 +35,12 @@ from qiskit.dagcircuit import DAGCircuit +DAGNode = qiskit._accelerate.circuit.DAGNode +DAGOpNode = qiskit._accelerate.circuit.DAGOpNode +DAGInNode = qiskit._accelerate.circuit.DAGInNode +DAGOutNode = qiskit._accelerate.circuit.DAGOutNode + + def _legacy_condition_eq(cond1, cond2, bit_indices1, bit_indices2) -> bool: if cond1 is cond2 is None: return True @@ -175,158 +177,65 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) -class DAGNode: - """Parent class for DAGOpNode, DAGInNode, and DAGOutNode.""" - - __slots__ = ["_node_id"] - - def __init__(self, nid=-1): - """Create a node""" - self._node_id = nid - - def __lt__(self, other): - return self._node_id < other._node_id - - def __gt__(self, other): - return self._node_id > other._node_id - - def __str__(self): - # TODO is this used anywhere other than in DAG drawing? - # needs to be unique as it is what pydot uses to distinguish nodes - return str(id(self)) - - @staticmethod - def semantic_eq(node1, node2, bit_indices1, bit_indices2): - """ - Check if DAG nodes are considered equivalent, e.g., as a node_match for - :func:`rustworkx.is_isomorphic_node_match`. - - Args: - node1 (DAGOpNode, DAGInNode, DAGOutNode): A node to compare. - node2 (DAGOpNode, DAGInNode, DAGOutNode): The other node to compare. - bit_indices1 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node1 - bit_indices2 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node2 - - Return: - Bool: If node1 == node2 - """ - if not isinstance(node1, DAGOpNode) or not isinstance(node1, DAGOpNode): - return type(node1) is type(node2) and bit_indices1.get(node1.wire) == bit_indices2.get( - node2.wire - ) - if isinstance(node1.op, ControlFlowOp) and isinstance(node2.op, ControlFlowOp): - # While control-flow operations aren't represented natively in the DAG, we have to do - # some unpleasant dispatching and very manual handling. Once they have more first-class - # support we'll still be dispatching, but it'll look more appropriate (like the dispatch - # based on `DAGOpNode`/`DAGInNode`/`DAGOutNode` that already exists) and less like we're - # duplicating code from the `ControlFlowOp` classes. - if type(node1.op) is not type(node2.op): - return False - comparer = _SEMANTIC_EQ_CONTROL_FLOW.get(type(node1.op)) - if comparer is None: # pragma: no cover - raise RuntimeError(f"unhandled control-flow operation: {type(node1.op)}") - return comparer(node1, node2, bit_indices1, bit_indices2) - - node1_qargs = [bit_indices1[qarg] for qarg in node1.qargs] - node1_cargs = [bit_indices1[carg] for carg in node1.cargs] - - node2_qargs = [bit_indices2[qarg] for qarg in node2.qargs] - node2_cargs = [bit_indices2[carg] for carg in node2.cargs] - - # For barriers, qarg order is not significant so compare as sets - if node1.op.name == node2.op.name and node1.name in _SEMANTIC_EQ_SYMMETRIC: - node1_qargs = set(node1_qargs) - node1_cargs = set(node1_cargs) - node2_qargs = set(node2_qargs) - node2_cargs = set(node2_cargs) - - return ( - node1_qargs == node2_qargs - and node1_cargs == node2_cargs - and _legacy_condition_eq( - getattr(node1.op, "condition", None), - getattr(node2.op, "condition", None), - bit_indices1, - bit_indices2, - ) - and node1.op == node2.op +# Note: called from dag_node.rs. +def _semantic_eq(node1, node2, bit_indices1, bit_indices2): + """ + Check if DAG nodes are considered equivalent, e.g., as a node_match for + :func:`rustworkx.is_isomorphic_node_match`. + + Args: + node1 (DAGOpNode, DAGInNode, DAGOutNode): A node to compare. + node2 (DAGOpNode, DAGInNode, DAGOutNode): The other node to compare. + bit_indices1 (dict): Dictionary mapping Bit instances to their index + within the circuit containing node1 + bit_indices2 (dict): Dictionary mapping Bit instances to their index + within the circuit containing node2 + + Return: + Bool: If node1 == node2 + """ + if not isinstance(node1, DAGOpNode) or not isinstance(node1, DAGOpNode): + return type(node1) is type(node2) and bit_indices1.get(node1.wire) == bit_indices2.get( + node2.wire ) + if isinstance(node1.op, ControlFlowOp) and isinstance(node2.op, ControlFlowOp): + # While control-flow operations aren't represented natively in the DAG, we have to do + # some unpleasant dispatching and very manual handling. Once they have more first-class + # support we'll still be dispatching, but it'll look more appropriate (like the dispatch + # based on `DAGOpNode`/`DAGInNode`/`DAGOutNode` that already exists) and less like we're + # duplicating code from the `ControlFlowOp` classes. + if type(node1.op) is not type(node2.op): + return False + comparer = _SEMANTIC_EQ_CONTROL_FLOW.get(type(node1.op)) + if comparer is None: # pragma: no cover + raise RuntimeError(f"unhandled control-flow operation: {type(node1.op)}") + return comparer(node1, node2, bit_indices1, bit_indices2) + + node1_qargs = [bit_indices1[qarg] for qarg in node1.qargs] + node1_cargs = [bit_indices1[carg] for carg in node1.cargs] + + node2_qargs = [bit_indices2[qarg] for qarg in node2.qargs] + node2_cargs = [bit_indices2[carg] for carg in node2.cargs] + + # For barriers, qarg order is not significant so compare as sets + if node1.op.name == node2.op.name and node1.name in _SEMANTIC_EQ_SYMMETRIC: + node1_qargs = set(node1_qargs) + node1_cargs = set(node1_cargs) + node2_qargs = set(node2_qargs) + node2_cargs = set(node2_cargs) + + return ( + node1_qargs == node2_qargs + and node1_cargs == node2_cargs + and _legacy_condition_eq( + getattr(node1.op, "condition", None), + getattr(node2.op, "condition", None), + bit_indices1, + bit_indices2, + ) + and node1.op == node2.op + ) -class DAGOpNode(DAGNode): - """Object to represent an Instruction at a node in the DAGCircuit.""" - - __slots__ = ["op", "qargs", "cargs", "sort_key"] - - def __init__( - self, op: Operation, qargs: Iterable[Qubit] = (), cargs: Iterable[Clbit] = (), dag=None - ): - """Create an Instruction node""" - super().__init__() - self.op = op - self.qargs = tuple(qargs) - self.cargs = tuple(cargs) - if dag is not None: - cache_key = (self.qargs, self.cargs) - key = dag._key_cache.get(cache_key, None) - if key is not None: - self.sort_key = key - else: - self.sort_key = ",".join( - f"{dag.find_bit(q).index:04d}" for q in itertools.chain(*cache_key) - ) - dag._key_cache[cache_key] = self.sort_key - else: - self.sort_key = str(self.qargs) - - @property - def name(self): - """Returns the Instruction name corresponding to the op for this node""" - return self.op.name - - @name.setter - def name(self, new_name): - """Sets the Instruction name corresponding to the op for this node""" - self.op.name = new_name - - def __repr__(self): - """Returns a representation of the DAGOpNode""" - return f"DAGOpNode(op={self.op}, qargs={self.qargs}, cargs={self.cargs})" - - -class DAGInNode(DAGNode): - """Object to represent an incoming wire node in the DAGCircuit.""" - - __slots__ = ["wire", "sort_key"] - - def __init__(self, wire): - """Create an incoming node""" - super().__init__() - self.wire = wire - # TODO sort_key which is used in dagcircuit.topological_nodes - # only works as str([]) for DAGInNodes. Need to figure out why. - self.sort_key = str([]) - - def __repr__(self): - """Returns a representation of the DAGInNode""" - return f"DAGInNode(wire={self.wire})" - - -class DAGOutNode(DAGNode): - """Object to represent an outgoing wire node in the DAGCircuit.""" - - __slots__ = ["wire", "sort_key"] - - def __init__(self, wire): - """Create an outgoing node""" - super().__init__() - self.wire = wire - # TODO sort_key which is used in dagcircuit.topological_nodes - # only works as str([]) for DAGOutNodes. Need to figure out why. - self.sort_key = str([]) - - def __repr__(self): - """Returns a representation of the DAGOutNode""" - return f"DAGOutNode(wire={self.wire})" +# Bind semantic_eq from Python to Rust implementation +DAGNode.semantic_eq = staticmethod(_semantic_eq) From 58a383d02a060d85da13b00fd9483da1e4f8139f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 14 May 2024 13:50:08 +0100 Subject: [PATCH 063/159] Fix `QuantumCircuit.compose` with `Index` exprs (#12396) This was an oversight in d6c74c265 (gh-12310), where an `ExprVisitor` was missed in the testing. --- qiskit/circuit/_classical_resource_map.py | 3 +++ test/python/circuit/test_compose.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py index 454826d6035..bff7d9f80fe 100644 --- a/qiskit/circuit/_classical_resource_map.py +++ b/qiskit/circuit/_classical_resource_map.py @@ -143,3 +143,6 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): return expr.Cast(node.operand.accept(self), node.type, implicit=node.implicit) + + def visit_index(self, node, /): + return expr.Index(node.target.accept(self), node.index.accept(self), node.type) diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 0e481c12b33..db6280b8823 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -820,13 +820,16 @@ def test_expr_condition_is_mapped(self): b_src = ClassicalRegister(2, "b_src") c_src = ClassicalRegister(name="c_src", bits=list(a_src) + list(b_src)) source = QuantumCircuit(QuantumRegister(1), a_src, b_src, c_src) + target_var = source.add_input("target_var", types.Uint(2)) test_1 = lambda: expr.lift(a_src[0]) test_2 = lambda: expr.logic_not(b_src[1]) test_3 = lambda: expr.logic_and(expr.bit_and(b_src, 2), expr.less(c_src, 7)) + test_4 = lambda: expr.bit_xor(expr.index(target_var, 0), expr.index(target_var, 1)) source.if_test(test_1(), inner.copy(), [0], []) source.if_else(test_2(), inner.copy(), inner.copy(), [0], []) source.while_loop(test_3(), inner.copy(), [0], []) + source.if_test(test_4(), inner.copy(), [0], []) a_dest = ClassicalRegister(2, "a_dest") b_dest = ClassicalRegister(2, "b_dest") @@ -840,12 +843,19 @@ def test_expr_condition_is_mapped(self): self.assertEqual(len(dest.cregs), 3) mapped_reg = dest.cregs[-1] - expected = QuantumCircuit(dest.qregs[0], a_dest, b_dest, mapped_reg) + expected = QuantumCircuit(dest.qregs[0], a_dest, b_dest, mapped_reg, inputs=[target_var]) expected.if_test(expr.lift(a_dest[0]), inner.copy(), [0], []) expected.if_else(expr.logic_not(b_dest[1]), inner.copy(), inner.copy(), [0], []) expected.while_loop( expr.logic_and(expr.bit_and(b_dest, 2), expr.less(mapped_reg, 7)), inner.copy(), [0], [] ) + # `Var` nodes aren't remapped, but this should be passed through fine. + expected.if_test( + expr.bit_xor(expr.index(target_var, 0), expr.index(target_var, 1)), + inner.copy(), + [0], + [], + ) self.assertEqual(dest, expected) def test_expr_target_is_mapped(self): From fe695946995ccd45059d678ffbc7852bcb489775 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 14 May 2024 16:26:27 +0100 Subject: [PATCH 064/159] Handle huge integers in OpenQASM 2 expression evaluator (#12140) * Handle huge integers in OpenQASM 2 expression evaluator This modifies the expression evaluator to directly parse the backing string data of an integer token in a floating-point context, which lets us handle numbers that would otherwise overflow a `usize`. It's possible for this to be totally valid, if, for example, the integer is a multiple of some very large power of two that doesn't overflow a double-precision float. We already needed to immediately cast the integer to a float, so this is just a more accurate way of doing the evaluation, and doesn't affect when we use integers in other contexts. * Clarify int/float split --- crates/qasm2/src/expr.rs | 14 ++++++++++++-- crates/qasm2/src/lex.rs | 4 ++-- .../notes/qasm2-bigint-8eff42acb67903e6.yaml | 9 +++++++++ test/python/qasm2/test_expression.py | 12 ++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index f7faad0c629..d8a08080a95 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -501,8 +501,13 @@ impl<'a> ExprParser<'a> { | TokenType::Sin | TokenType::Sqrt | TokenType::Tan => Ok(Some(Atom::Function(token.ttype.into()))), - TokenType::Real => Ok(Some(Atom::Const(token.real(self.context)))), - TokenType::Integer => Ok(Some(Atom::Const(token.int(self.context) as f64))), + // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0 + // integers can be interpreted as floats, and doing that allows us to gracefully handle + // cases where a huge float would overflow a `usize`. Never mind that in such a case, + // there's almost certainly precision loss from the floating-point representating + // having insufficient mantissa digits to faithfully represent the angle mod 2pi; + // that's not our fault in the parser. + TokenType::Real | TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))), TokenType::Id => { let id = token.text(self.context); @@ -698,6 +703,11 @@ impl<'a> ExprParser<'a> { /// Parse a single expression completely. This is the only public entry point to the /// operator-precedence parser. + /// + /// .. note:: + /// + /// This evaluates in a floating-point context, including evaluating integer tokens, since + /// the only places that expressions are valid in OpenQASM 2 is during gate applications. pub fn parse_expression(&mut self, cause: &Token) -> PyResult { self.eval_expression(0, cause) } diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index 024681b877f..f9f674cbc93 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -262,9 +262,9 @@ impl Token { } /// If the token is a real number, this method can be called to evaluate its value. Panics if - /// the token is not a real number. + /// the token is not a float or an integer. pub fn real(&self, context: &TokenContext) -> f64 { - if self.ttype != TokenType::Real { + if !(self.ttype == TokenType::Real || self.ttype == TokenType::Integer) { panic!() } context.text[self.index].parse().unwrap() diff --git a/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml new file mode 100644 index 00000000000..2fb1b4dcc5a --- /dev/null +++ b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The OpenQASM 2.0 parser (:func:`.qasm2.load` and :func:`.qasm2.loads`) can now evaluate + gate-angle expressions including integer operands that would overflow the system-size integer. + These will be evaluated in a double-precision floating-point context, just like the rest of the + expression always has been. Beware: an arbitrarily large integer will not necessarily be + exactly representable in double-precision floating-point, so there is a chance that however the + circuit was generated, it had already lost all numerical precision modulo :math:`2\pi`. diff --git a/test/python/qasm2/test_expression.py b/test/python/qasm2/test_expression.py index 98aead7f3b4..2ef35abde9e 100644 --- a/test/python/qasm2/test_expression.py +++ b/test/python/qasm2/test_expression.py @@ -123,6 +123,18 @@ def test_function_symbolic(self, function_str, function_py): actual = [float(x) for x in abstract_op.definition.data[0].operation.params] self.assertEqual(list(actual), expected) + def test_bigint(self): + """Test that an expression can be evaluated even if it contains an integer that will + overflow the integer handling.""" + bigint = 1 << 200 + # Sanity check that the number we're trying for is represented at full precision in floating + # point (which it should be - it's a power of two with fewer than 11 bits of exponent). + self.assertEqual(int(float(bigint)), bigint) + program = f"qreg q[1]; U({bigint}, -{bigint}, {bigint} * 2.0) q[0];" + parsed = qiskit.qasm2.loads(program) + parameters = list(parsed.data[0].operation.params) + self.assertEqual([bigint, -bigint, 2 * bigint], parameters) + class TestPrecedenceAssociativity(QiskitTestCase): def test_precedence(self): From cea93a051671b4c56945e916a70d22c2f5d488d8 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Tue, 14 May 2024 20:09:01 +0400 Subject: [PATCH 065/159] Fix a corner case of `SparsePauliOp.apply_layout` (#12375) * fix a corner case of `SparsePauliOp.apply_layout` * Add zero-qubit tests of Pauli.apply_layout * use combine and apply isort * Update releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml --------- Co-authored-by: Matthew Treinish --- .../operators/symplectic/sparse_pauli_op.py | 2 + ...op-apply-layout-zero-43b9e70f0d1536a6.yaml | 10 +++++ .../operators/symplectic/test_pauli.py | 40 +++++++++++-------- .../symplectic/test_sparse_pauli_op.py | 31 ++++++++++---- 4 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index cf51579bef8..440a0319c33 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -1165,6 +1165,8 @@ def apply_layout( raise QiskitError("Provided layout contains indices outside the number of qubits.") if len(set(layout)) != len(layout): raise QiskitError("Provided layout contains duplicate indices.") + if self.num_qubits == 0: + return type(self)(["I" * n_qubits] * self.size, self.coeffs) new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml b/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml new file mode 100644 index 00000000000..117230aee53 --- /dev/null +++ b/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml @@ -0,0 +1,10 @@ +fixes: + - | + Fixed :meth:`.SparsePauliOp.apply_layout` to work correctly with zero-qubit operators. + For example, if you previously created a 0 qubit and applied a layout like:: + + op = SparsePauliOp("") + op.apply_layout(None, 3) + + this would have previously raised an error. Now this will correctly return an operator of the form: + ``SparsePauliOp(['III'], coeffs=[1.+0.j])`` diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py index 89324e8212e..35acd46a4d0 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli.py @@ -14,42 +14,41 @@ """Tests for Pauli operator class.""" +import itertools as it import re import unittest -import itertools as it from functools import lru_cache +from test import QiskitTestCase, combine + import numpy as np -from ddt import ddt, data, unpack +from ddt import data, ddt, unpack from qiskit import QuantumCircuit from qiskit.circuit import Qubit -from qiskit.exceptions import QiskitError from qiskit.circuit.library import ( - IGate, - XGate, - YGate, - ZGate, - HGate, - SGate, - SdgGate, CXGate, - CZGate, CYGate, - SwapGate, + CZGate, ECRGate, EfficientSU2, + HGate, + IGate, + SdgGate, + SGate, + SwapGate, + XGate, + YGate, + ZGate, ) from qiskit.circuit.library.generalized_gates import PauliGate from qiskit.compiler.transpiler import transpile -from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.exceptions import QiskitError from qiskit.primitives import BackendEstimator +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.quantum_info.operators import Operator, Pauli, SparsePauliOp from qiskit.quantum_info.random import random_clifford, random_pauli -from qiskit.quantum_info.operators import Pauli, Operator, SparsePauliOp from qiskit.utils import optionals -from test import QiskitTestCase # pylint: disable=wrong-import-order - - LABEL_REGEX = re.compile(r"(?P[+-]?1?[ij]?)(?P[IXYZ]*)") PHASE_MAP = {"": 0, "-i": 1, "-": 2, "i": 3} @@ -618,6 +617,13 @@ def test_apply_layout_duplicate_indices(self): with self.assertRaises(QiskitError): op.apply_layout(layout=[0, 0], num_qubits=3) + @combine(phase=["", "-i", "-", "i"], layout=[None, []]) + def test_apply_layout_zero_qubit(self, phase, layout): + """Test apply_layout with a zero-qubit operator""" + op = Pauli(phase) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(Pauli(phase + "IIIII"), res) + if __name__ == "__main__": unittest.main() diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 1149ef1f346..e7a9b89b731 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -14,23 +14,22 @@ import itertools as it import unittest +from test import QiskitTestCase, combine + import numpy as np -import scipy.sparse import rustworkx as rx +import scipy.sparse from ddt import ddt - from qiskit import QiskitError -from qiskit.circuit import ParameterExpression, Parameter, ParameterVector -from qiskit.circuit.parametertable import ParameterView -from qiskit.quantum_info.operators import Operator, Pauli, PauliList, SparsePauliOp +from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit.library import EfficientSU2 +from qiskit.circuit.parametertable import ParameterView +from qiskit.compiler.transpiler import transpile from qiskit.primitives import BackendEstimator from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.compiler.transpiler import transpile +from qiskit.quantum_info.operators import Operator, Pauli, PauliList, SparsePauliOp from qiskit.utils import optionals -from test import QiskitTestCase # pylint: disable=wrong-import-order -from test import combine # pylint: disable=wrong-import-order def pauli_mat(label): @@ -1191,6 +1190,22 @@ def test_apply_layout_duplicate_indices(self): with self.assertRaises(QiskitError): op.apply_layout(layout=[0, 0], num_qubits=3) + @combine(layout=[None, []]) + def test_apply_layout_zero_qubit(self, layout): + """Test apply_layout with a zero-qubit operator""" + with self.subTest("default"): + op = SparsePauliOp("") + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp("IIIII"), res) + with self.subTest("coeff"): + op = SparsePauliOp("", 2) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp("IIIII", 2), res) + with self.subTest("multiple ops"): + op = SparsePauliOp.from_list([("", 1), ("", 2)]) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp.from_list([("IIIII", 1), ("IIIII", 2)]), res) + if __name__ == "__main__": unittest.main() From 07c8f20481813493eae80164731794fccdc1de3d Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Tue, 14 May 2024 16:47:29 -0400 Subject: [PATCH 066/159] Fix two cross-references in `BasisTranslator` docstring (#12398) --- qiskit/transpiler/passes/basis/basis_translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 074c6d341ba..04bf852e55b 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -97,8 +97,8 @@ class BasisTranslator(TransformationPass): When this error occurs it typically means that either the target basis is not universal or there are additional equivalence rules needed in the - :clas:~.EquivalenceLibrary` instance being used by the - :class:~.BasisTranslator` pass. You can refer to + :class:`~.EquivalenceLibrary` instance being used by the + :class:`~.BasisTranslator` pass. You can refer to :ref:`custom_basis_gates` for details on adding custom equivalence rules. """ From c6c45a1de9140d3051f34a90a8486e689f0a8749 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Tue, 14 May 2024 23:37:41 -0400 Subject: [PATCH 067/159] export SamplerPubResult in qiskit.primitives module (#12406) --- qiskit/primitives/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 2423f3545f8..569c075b23b 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -417,6 +417,7 @@ DataBin PrimitiveResult PubResult + SamplerPubResult BasePrimitiveJob PrimitiveJob @@ -466,6 +467,7 @@ PubResult, EstimatorPubLike, SamplerPubLike, + SamplerPubResult, BindingsArrayLike, ObservableLike, ObservablesArrayLike, From d4522251d33fe34419a725c6546f3268df76e907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 09:40:38 -0400 Subject: [PATCH 068/159] Bump pulp from 0.18.10 to 0.18.12 (#12409) Bumps [pulp](https://github.com/sarah-ek/pulp) from 0.18.10 to 0.18.12. - [Commits](https://github.com/sarah-ek/pulp/commits) --- updated-dependencies: - dependency-name: pulp 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 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6915167f9c..d812f8fc1c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "pulp" -version = "0.18.10" +version = "0.18.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14989307e408d9f4245d4fda09a7b144a08114ba124e26cab60ab83dc98db10" +checksum = "140dfe6dada20716bd5f7284406747c73061a56a0a5d4ad5aee7957c5f71606c" dependencies = [ "bytemuck", "libm", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index a43fdc6ff50..4f2c80ebff2 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -53,5 +53,5 @@ version = "0.1.0" features = ["ndarray"] [dependencies.pulp] -version = "0.18.10" +version = "0.18.12" features = ["macro"] From b12e9ec3cff020983e3dde9b16f5ccc4fd0f4963 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 15 May 2024 17:56:11 +0100 Subject: [PATCH 069/159] Fix unnecessary serialisation of `PassManager` in serial contexts (#12410) * Fix unnecessary serialisation of `PassManager` in serial contexts This exposes the interal decision in `parallel_map` of whether to actually run in serial or not. If not, there's no need for `PassManager` to side-car its `dill` serialisation onto the side of the IPC (we use `dill` because we need to pickle lambdas), which can be an unfortunately huge cost for certain IBM pulse-enabled backends. * Remove new function from public API This makes the patch series safe for backport to 1.1. --- qiskit/passmanager/passmanager.py | 22 +++++------ qiskit/utils/__init__.py | 5 ++- qiskit/utils/parallel.py | 39 ++++++++++++------- .../parallel-check-8186a8f074774a1f.yaml | 5 +++ 4 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/parallel-check-8186a8f074774a1f.yaml diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py index ba416dfb063..8d3a4e9aa69 100644 --- a/qiskit/passmanager/passmanager.py +++ b/qiskit/passmanager/passmanager.py @@ -21,7 +21,7 @@ import dill -from qiskit.utils.parallel import parallel_map +from qiskit.utils.parallel import parallel_map, should_run_in_parallel from .base_tasks import Task, PassManagerIR from .exceptions import PassManagerError from .flow_controllers import FlowControllerLinear @@ -225,16 +225,16 @@ def callback_func(**kwargs): in_programs = [in_programs] is_list = False - if len(in_programs) == 1: - out_program = _run_workflow( - program=in_programs[0], - pass_manager=self, - callback=callback, - **kwargs, - ) - if is_list: - return [out_program] - return out_program + # If we're not going to run in parallel, we want to avoid spending time `dill` serialising + # ourselves, since that can be quite expensive. + if len(in_programs) == 1 or not should_run_in_parallel(num_processes): + out = [ + _run_workflow(program=program, pass_manager=self, callback=callback, **kwargs) + for program in in_programs + ] + if len(in_programs) == 1 and not is_list: + return out[0] + return out del callback del kwargs diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index f5256f6f11e..30935437ebf 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -44,7 +44,7 @@ .. autofunction:: local_hardware_info .. autofunction:: is_main_process -A helper function for calling a custom function with python +A helper function for calling a custom function with Python :class:`~concurrent.futures.ProcessPoolExecutor`. Tasks can be executed in parallel using this function. .. autofunction:: parallel_map @@ -70,7 +70,7 @@ from . import optionals -from .parallel import parallel_map +from .parallel import parallel_map, should_run_in_parallel __all__ = [ "LazyDependencyManager", @@ -85,4 +85,5 @@ "is_main_process", "apply_prefix", "parallel_map", + "should_run_in_parallel", ] diff --git a/qiskit/utils/parallel.py b/qiskit/utils/parallel.py index d46036a478f..f87eeb81596 100644 --- a/qiskit/utils/parallel.py +++ b/qiskit/utils/parallel.py @@ -48,6 +48,8 @@ from the multiprocessing library. """ +from __future__ import annotations + import os from concurrent.futures import ProcessPoolExecutor import sys @@ -101,6 +103,21 @@ def _task_wrapper(param): return task(value, *task_args, **task_kwargs) +def should_run_in_parallel(num_processes: int | None = None) -> bool: + """Return whether the current parallelisation configuration suggests that we should run things + like :func:`parallel_map` in parallel (``True``) or degrade to serial (``False``). + + Args: + num_processes: the number of processes requested for use (if given). + """ + num_processes = CPU_COUNT if num_processes is None else num_processes + return ( + num_processes > 1 + and os.getenv("QISKIT_IN_PARALLEL", "FALSE") == "FALSE" + and CONFIG.get("parallel_enabled", PARALLEL_DEFAULT) + ) + + def parallel_map( # pylint: disable=dangerous-default-value task, values, task_args=(), task_kwargs={}, num_processes=CPU_COUNT ): @@ -110,21 +127,20 @@ def parallel_map( # pylint: disable=dangerous-default-value result = [task(value, *task_args, **task_kwargs) for value in values] - On Windows this function defaults to a serial implementation to avoid the - overhead from spawning processes in Windows. + This will parallelise the results if the number of ``values`` is greater than one, and the + current system configuration permits parallelization. Args: task (func): Function that is to be called for each value in ``values``. - values (array_like): List or array of values for which the ``task`` - function is to be evaluated. + values (array_like): List or array of values for which the ``task`` function is to be + evaluated. task_args (list): Optional additional arguments to the ``task`` function. task_kwargs (dict): Optional additional keyword argument to the ``task`` function. num_processes (int): Number of processes to spawn. Returns: - result: The result list contains the value of - ``task(value, *task_args, **task_kwargs)`` for - each value in ``values``. + result: The result list contains the value of ``task(value, *task_args, **task_kwargs)`` for + each value in ``values``. Raises: QiskitError: If user interrupts via keyboard. @@ -147,12 +163,7 @@ def func(_): if len(values) == 1: return [task(values[0], *task_args, **task_kwargs)] - # Run in parallel if not Win and not in parallel already - if ( - num_processes > 1 - and os.getenv("QISKIT_IN_PARALLEL") == "FALSE" - and CONFIG.get("parallel_enabled", PARALLEL_DEFAULT) - ): + if should_run_in_parallel(num_processes): os.environ["QISKIT_IN_PARALLEL"] = "TRUE" try: results = [] @@ -173,8 +184,6 @@ def func(_): os.environ["QISKIT_IN_PARALLEL"] = "FALSE" return results - # Cannot do parallel on Windows , if another parallel_map is running in parallel, - # or len(values) == 1. results = [] for _, value in enumerate(values): result = task(value, *task_args, **task_kwargs) diff --git a/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml b/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml new file mode 100644 index 00000000000..d3266b2aa5f --- /dev/null +++ b/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + :meth:`.PassManager.run` will no longer waste time serializing itself when given multiple inputs + if it is only going to work in serial. From 96607f6b2068594f216928b954db0f516e1466ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iy=C3=A1n?= Date: Thu, 16 May 2024 08:52:54 +0200 Subject: [PATCH 070/159] Avoid lossing precision when scaling frequencies (#12392) * Avoid lossing precision when scaling frequencies Classes in pulse_instruction.py scale frequency values to GHz by multipliying `ParameterExpression` with float 1e9. This can lead to numerical errors on some systems using symengine. Instead, this scaling can be done multiplying by integer 10**9. See: https://github.com/Qiskit/qiskit/issues/12359#issuecomment-2104426621 * Add release note --------- Co-authored-by: Jake Lishman --- qiskit/qobj/converters/pulse_instruction.py | 12 ++++++------ .../fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 77e811100f3..80c332aaab4 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -234,7 +234,7 @@ def _convert_set_frequency( "name": "setf", "t0": time_offset + instruction.start_time, "ch": instruction.channel.name, - "frequency": instruction.frequency / 1e9, + "frequency": instruction.frequency / 10**9, } return self._qobj_model(**command_dict) @@ -257,7 +257,7 @@ def _convert_shift_frequency( "name": "shiftf", "t0": time_offset + instruction.start_time, "ch": instruction.channel.name, - "frequency": instruction.frequency / 1e9, + "frequency": instruction.frequency / 10**9, } return self._qobj_model(**command_dict) @@ -746,7 +746,7 @@ def _convert_setf( .. note:: We assume frequency value is expressed in string with "GHz". - Operand value is thus scaled by a factor of 1e9. + Operand value is thus scaled by a factor of 10^9. Args: instruction: SetFrequency qobj instruction @@ -755,7 +755,7 @@ def _convert_setf( Qiskit Pulse set frequency instructions """ channel = self.get_channel(instruction.ch) - frequency = self.disassemble_value(instruction.frequency) * 1e9 + frequency = self.disassemble_value(instruction.frequency) * 10**9 yield instructions.SetFrequency(frequency, channel) @@ -768,7 +768,7 @@ def _convert_shiftf( .. note:: We assume frequency value is expressed in string with "GHz". - Operand value is thus scaled by a factor of 1e9. + Operand value is thus scaled by a factor of 10^9. Args: instruction: ShiftFrequency qobj instruction @@ -777,7 +777,7 @@ def _convert_shiftf( Qiskit Pulse shift frequency schedule instructions """ channel = self.get_channel(instruction.ch) - frequency = self.disassemble_value(instruction.frequency) * 1e9 + frequency = self.disassemble_value(instruction.frequency) * 10**9 yield instructions.ShiftFrequency(frequency, channel) diff --git a/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml b/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml new file mode 100644 index 00000000000..5ca00904a9a --- /dev/null +++ b/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a floating-point imprecision when scaling certain pulse units + between seconds and nanoseconds. If the pulse was symbolically defined, + an unnecessary floating-point error could be introduced by the scaling + for certain builds of ``symengine``, which could manifest in unexpected + results once the symbols were fully bound. See `#12392 `__. From 72ad545a87e1cbd79afe4bbc64f5b835a8e92f7e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 16 May 2024 14:40:46 +0100 Subject: [PATCH 071/159] Write complete manual `QuantumCircuit` documentation (#12403) * Write complete manual `QuantumCircuit` documentation This writes huge tracts of new `QuantumCircuit` API documentation, linking together alike methods and writing explanatory text for how all the components fit together. There's likely an awful lot more that could go into this too, but this hopefully should impose a lot more order on the huge `QuantumCircuit` documentation page, and provide a lot more explanation for how the class works holistically. In particular, the section on the control-flow builder interface could do with a lot more exposition and examples right now. * Reword from review Co-authored-by: Matthew Treinish * Add missing text around `Var` methods * Add example to `find_bit` * Comment on metadata through serialization * Add see-also sections to undesirable methods * Re-add internal utilities to documentation * Add examples to `depth` --------- Co-authored-by: Matthew Treinish --- docs/apidoc/index.rst | 1 + docs/apidoc/qiskit.circuit.QuantumCircuit.rst | 17 + qiskit/circuit/__init__.py | 163 +-- qiskit/circuit/quantumcircuit.py | 1216 +++++++++++++++-- 4 files changed, 1098 insertions(+), 299 deletions(-) create mode 100644 docs/apidoc/qiskit.circuit.QuantumCircuit.rst diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 30bb20998d6..8581d56ace7 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -13,6 +13,7 @@ Circuit construction: :maxdepth: 1 circuit + qiskit.circuit.QuantumCircuit circuit_classical classicalfunction circuit_library diff --git a/docs/apidoc/qiskit.circuit.QuantumCircuit.rst b/docs/apidoc/qiskit.circuit.QuantumCircuit.rst new file mode 100644 index 00000000000..1fa9cb5a7d9 --- /dev/null +++ b/docs/apidoc/qiskit.circuit.QuantumCircuit.rst @@ -0,0 +1,17 @@ +.. _qiskit-circuit-quantumcircuit: + +============================== +:class:`.QuantumCircuit` class +============================== + +.. + This is so big it gets its own page in the toctree, and because we + don't want it to use autosummary. + +.. currentmodule:: qiskit.circuit + +.. autoclass:: qiskit.circuit.QuantumCircuit + :no-members: + :no-inherited-members: + :no-special-members: + :class-doc-from: class diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index ff36550967d..3982fa87334 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -321,16 +321,6 @@ :class:`QuantumCircuit` class itself and the multitude of available methods on it in its class documentation. -.. - TODO: the intention is to replace this `autosummary` directive with a proper entry in the API - toctree once the `QuantumCircuit` class-level documentation has been completely rewritten into - more of this style. For now, this just ensures it gets *any* page generated. - -.. autosummary:: - :toctree: ../stubs/ - - QuantumCircuit - Internally, a :class:`QuantumCircuit` contains the qubits, classical bits, compile-time parameters, real-time variables, and other tracking information about the data it acts on and how it is parametrized. It then contains a sequence of :class:`CircuitInstruction`\ s, which contain @@ -390,7 +380,7 @@ Circuits track registers, but registers themselves impart almost no behavioral differences on circuits. The only exception is that :class:`ClassicalRegister`\ s can be implicitly cast to unsigned integers for use in conditional comparisons of :ref:`control flow operations -`. +`. Classical registers and bits were the original way of representing classical data in Qiskit, and remain the most supported currently. Longer term, the data model is moving towards a more complete @@ -433,6 +423,8 @@ circuit), but these are now discouraged and you should use the alternatives noted in those methods. +.. _circuit-operations-instructions: + Operations, instructions and gates ---------------------------------- @@ -598,17 +590,14 @@ Real-time classical computation ------------------------------- -.. note:: +.. seealso:: + :mod:`qiskit.circuit.classical` + Module-level documentation for how the variable-, expression- and type-systems work, the + objects used to represent them, and the classical operations available. - The primary documentation for real-time classical computation is in the module-level - documentation of :mod:`qiskit.circuit.classical`. - - You might also want to read about the circuit methods for working with real-time variables on - the :class:`QuantumCircuit` class page. - - .. - TODO: write a section in the QuantumCircuit-level guide about real-time-variable methods and - cross-ref to it. + :ref:`circuit-real-time-methods` + The :class:`QuantumCircuit` methods for working with these variables in the context of a + single circuit. Qiskit has rudimentary low-level support for representing real-time classical computations, which happen during the QPU execution and affect the results. We are still relatively early into hardware @@ -674,7 +663,7 @@ ParameterVector -.. _circuit-control-flow: +.. _circuit-control-flow-repr: Control flow in circuits ------------------------ @@ -718,11 +707,8 @@ The classes representations are documented here, but please note that manually constructing these classes is a low-level operation that we do not expect users to need to do frequently. - .. - TODO: make this below statement valid, and reinsert. - - Users should read :ref:`circuit-creating-control-flow` for the recommended workflows for - building control-flow-enabled circuits. + Users should read :ref:`circuit-control-flow-methods` for the recommended workflows for building + control-flow-enabled circuits. Since :class:`ControlFlowOp` subclasses are also :class:`Instruction` subclasses, this means that the way they are stored in :class:`CircuitInstruction` instances has them "applied" to a sequence of @@ -772,11 +758,8 @@ argument), but user code will typically use the control-flow builder interface, which handles this automatically. -.. - TODO: make the below sentence valid, then re-insert. - - Consult :ref:`the control-flow construction documentation ` for - more information on how to build circuits with control flow. +Consult :ref:`the control-flow construction documentation ` for more +information on how to build circuits with control flow. .. _circuit-custom-gates: @@ -920,122 +903,6 @@ def __array__(self, dtype=None, copy=None): Working with circuit-level objects ================================== -Circuit properties ------------------- - -.. - TODO: rewrite this section and move it into the `QuantumCircuit` class-level overview of its - functions. - -When constructing quantum circuits, there are several properties that help quantify -the "size" of the circuits, and their ability to be run on a noisy quantum device. -Some of these, like number of qubits, are straightforward to understand, while others -like depth and number of tensor components require a bit more explanation. Here we will -explain all of these properties, and, in preparation for understanding how circuits change -when run on actual devices, highlight the conditions under which they change. - -Consider the following circuit: - -.. plot:: - :include-source: - - from qiskit import QuantumCircuit - qc = QuantumCircuit(12) - for idx in range(5): - qc.h(idx) - qc.cx(idx, idx+5) - - qc.cx(1, 7) - qc.x(8) - qc.cx(1, 9) - qc.x(7) - qc.cx(1, 11) - qc.swap(6, 11) - qc.swap(6, 9) - qc.swap(6, 10) - qc.x(6) - qc.draw('mpl') - -From the plot, it is easy to see that this circuit has 12 qubits, and a collection of -Hadamard, CNOT, X, and SWAP gates. But how to quantify this programmatically? Because we -can do single-qubit gates on all the qubits simultaneously, the number of qubits in this -circuit is equal to the **width** of the circuit: - -.. code-block:: - - qc.width() - -.. parsed-literal:: - - 12 - -We can also just get the number of qubits directly: - -.. code-block:: - - qc.num_qubits - -.. parsed-literal:: - - 12 - -.. important:: - - For a quantum circuit composed from just qubits, the circuit width is equal - to the number of qubits. This is the definition used in quantum computing. However, - for more complicated circuits with classical registers, and classically controlled gates, - this equivalence breaks down. As such, from now on we will not refer to the number of - qubits in a quantum circuit as the width. - - -It is also straightforward to get the number and type of the gates in a circuit using -:meth:`QuantumCircuit.count_ops`: - -.. code-block:: - - qc.count_ops() - -.. parsed-literal:: - - OrderedDict([('cx', 8), ('h', 5), ('x', 3), ('swap', 3)]) - -We can also get just the raw count of operations by computing the circuits -:meth:`QuantumCircuit.size`: - -.. code-block:: - - qc.size() - -.. parsed-literal:: - - 19 - -A particularly important circuit property is known as the circuit **depth**. The depth -of a quantum circuit is a measure of how many "layers" of quantum gates, executed in -parallel, it takes to complete the computation defined by the circuit. Because quantum -gates take time to implement, the depth of a circuit roughly corresponds to the amount of -time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit -is one important quantity used to measure if a quantum circuit can be run on a device. - -The depth of a quantum circuit has a mathematical definition as the longest path in a -directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for -experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar -with playing `Tetris `_. Lets see how to compute this -graphically: - -.. image:: /source_images/depth.gif - - -We can verify our graphical result using :meth:`QuantumCircuit.depth`: - -.. code-block:: - - qc.depth() - -.. parsed-literal:: - - 9 - .. _circuit-abstract-to-physical: Converting abstract circuits to physical circuits diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index abd48c686b1..a157f04375a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -106,113 +106,878 @@ BitType = TypeVar("BitType", Qubit, Clbit) +# NOTE: +# +# If you're adding methods or attributes to `QuantumCircuit`, be sure to update the class docstring +# to document them in a suitable place. The class is huge, so we do its documentation manually so +# it has at least some amount of organisational structure. + + class QuantumCircuit: - """Create a new circuit. + """Core Qiskit representation of a quantum circuit. + + .. note:: + For more details setting the :class:`QuantumCircuit` in context of all of the data + structures that go with it, how it fits into the rest of the :mod:`qiskit` package, and the + different regimes of quantum-circuit descriptions in Qiskit, see the module-level + documentation of :mod:`qiskit.circuit`. + + Circuit attributes + ================== + + :class:`QuantumCircuit` has a small number of public attributes, which are mostly older + functionality. Most of its functionality is accessed through methods. + + A small handful of the attributes are intentionally mutable, the rest are data attributes that + should be considered immutable. + + ========================= ====================================================================== + Mutable attribute Summary + ========================= ====================================================================== + :attr:`global_phase` The global phase of the circuit, measured in radians. + :attr:`metadata` Arbitrary user mapping, which Qiskit will preserve through the + transpiler, but otherwise completely ignore. + :attr:`name` An optional string name for the circuit. + ========================= ====================================================================== + + ========================= ====================================================================== + Immutable data attribute Summary + ========================= ====================================================================== + :attr:`ancillas` List of :class:`AncillaQubit`\\ s tracked by the circuit. + :attr:`calibrations` Custom user-supplied pulse calibrations for individual instructions. + :attr:`cregs` List of :class:`ClassicalRegister`\\ s tracked by the circuit. + + :attr:`clbits` List of :class:`Clbit`\\ s tracked by the circuit. + :attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the + circuit. + :attr:`duration` Total duration of the circuit, added by scheduling transpiler passes. + + :attr:`layout` Hardware layout and routing information added by the transpiler. + :attr:`num_ancillas` The number of ancilla qubits in the circuit. + :attr:`num_clbits` The number of clbits in the circuit. + :attr:`num_captured_vars` Number of captured real-time classical variables. + + :attr:`num_declared_vars` Number of locally declared real-time classical variables in the outer + circuit scope. + :attr:`num_input_vars` Number of input real-time classical variables. + :attr:`num_parameters` Number of compile-time :class:`Parameter`\\ s in the circuit. + :attr:`num_qubits` Number of qubits in the circuit. + + :attr:`num_vars` Total number of real-time classical variables in the outer circuit + scope. + :attr:`op_start_times` Start times of scheduled operations, added by scheduling transpiler + passes. + :attr:`parameters` Ordered set-like view of the compile-time :class:`Parameter`\\ s + tracked by the circuit. + :attr:`qregs` List of :class:`QuantumRegister`\\ s tracked by the circuit. + + :attr:`qubits` List of :class:`Qubit`\\ s tracked by the circuit. + :attr:`unit` The unit of the :attr:`duration` field. + ========================= ====================================================================== + + The core attribute is :attr:`data`. This is a sequence-like object that exposes the + :class:`CircuitInstruction`\\ s contained in an ordered form. You generally should not mutate + this object directly; :class:`QuantumCircuit` is only designed for append-only operations (which + should use :meth:`append`). Most operations that mutate circuits in place should be written as + transpiler passes (:mod:`qiskit.transpiler`). + + .. autoattribute:: data + + Alongside the :attr:`data`, the :attr:`global_phase` of a circuit can have some impact on its + output, if the circuit is used to describe a :class:`.Gate` that may be controlled. This is + measured in radians and is directly settable. + + .. autoattribute:: global_phase + + The :attr:`name` of a circuit becomes the name of the :class:`~.circuit.Instruction` or + :class:`.Gate` resulting from :meth:`to_instruction` and :meth:`to_gate` calls, which can be + handy for visualizations. + + .. autoattribute:: name + + You can attach arbitrary :attr:`metadata` to a circuit. No part of core Qiskit will inspect + this or change its behavior based on metadata, but it will be faithfully passed through the + transpiler, so you can tag your circuits yourself. When serializing a circuit with QPY (see + :mod:`qiskit.qpy`), the metadata will be JSON-serialized and you may need to pass a custom + serializer to handle non-JSON-compatible objects within it (see :func:`.qpy.dump` for more + detail). This field is ignored during export to OpenQASM 2 or 3. + + .. autoattribute:: metadata + + :class:`QuantumCircuit` exposes data attributes tracking its internal quantum and classical bits + and registers. These appear as Python :class:`list`\\ s, but you should treat them as + immutable; changing them will *at best* have no effect, and more likely will simply corrupt + the internal data of the :class:`QuantumCircuit`. + + .. autoattribute:: qregs + .. autoattribute:: cregs + .. autoattribute:: qubits + .. autoattribute:: ancillas + .. autoattribute:: clbits + + The :ref:`compile-time parameters ` present in instructions on + the circuit are available in :attr:`parameters`. This has a canonical order (mostly lexical, + except in the case of :class:`.ParameterVector`), which matches the order that parameters will + be assigned when using the list forms of :meth:`assign_parameters`, but also supports + :class:`set`-like constant-time membership testing. + + .. autoattribute:: parameters + + The storage of any :ref:`manual pulse-level calibrations ` for individual + instructions on the circuit is in :attr:`calibrations`. This presents as a :class:`dict`, but + should not be mutated directly; use the methods discussed in :ref:`circuit-calibrations`. + + .. autoattribute:: calibrations + + If you have transpiled your circuit, so you have a physical circuit, you can inspect the + :attr:`layout` attribute for information stored by the transpiler about how the virtual qubits + of the source circuit map to the hardware qubits of your physical circuit, both at the start and + end of the circuit. + + .. autoattribute:: layout + + If your circuit was also *scheduled* as part of a transpilation, it will expose the individual + timings of each instruction, along with the total :attr:`duration` of the circuit. + + .. autoattribute:: duration + .. autoattribute:: unit + .. autoattribute:: op_start_times + + Finally, :class:`QuantumCircuit` exposes several simple properties as dynamic read-only numeric + attributes. + + .. autoattribute:: num_ancillas + .. autoattribute:: num_clbits + .. autoattribute:: num_captured_vars + .. autoattribute:: num_declared_vars + .. autoattribute:: num_input_vars + .. autoattribute:: num_parameters + .. autoattribute:: num_qubits + .. autoattribute:: num_vars + + Creating new circuits + ===================== + + ========================= ===================================================================== + Method Summary + ========================= ===================================================================== + :meth:`__init__` Default constructor of no-instruction circuits. + :meth:`copy` Make a complete copy of an existing circuit. + :meth:`copy_empty_like` Copy data objects from one circuit into a new one without any + instructions. + :meth:`from_instructions` Infer data objects needed from a list of instructions. + :meth:`from_qasm_file` Legacy interface to :func:`.qasm2.load`. + :meth:`from_qasm_str` Legacy interface to :func:`.qasm2.loads`. + ========================= ===================================================================== + + The default constructor (``QuantumCircuit(...)``) produces a circuit with no initial + instructions. The arguments to the default constructor can be used to seed the circuit with + quantum and classical data storage, and to provide a name, global phase and arbitrary metadata. + All of these fields can be expanded later. + + .. automethod:: __init__ + + If you have an existing circuit, you can produce a copy of it using :meth:`copy`, including all + its instructions. This is useful if you want to keep partial circuits while extending another, + or to have a version you can mutate in-place while leaving the prior one intact. - A circuit is a list of instructions bound to some registers. + .. automethod:: copy - Args: - regs (list(:class:`~.Register`) or list(``int``) or list(list(:class:`~.Bit`))): The - registers to be included in the circuit. + Similarly, if you want a circuit that contains all the same data objects (bits, registers, + variables, etc) but with none of the instructions, you can use :meth:`copy_empty_like`. This is + quite common when you want to build up a new layer of a circuit to then use apply onto the back + with :meth:`compose`, or to do a full rewrite of a circuit's instructions. + + .. automethod:: copy_empty_like + + In some cases, it is most convenient to generate a list of :class:`.CircuitInstruction`\\ s + separately to an entire circuit context, and then to build a circuit from this. The + :meth:`from_instructions` constructor will automatically capture all :class:`.Qubit` and + :class:`.Clbit` instances used in the instructions, and create a new :class:`QuantumCircuit` + object that has the correct resources and all the instructions. + + .. automethod:: from_instructions + + :class:`QuantumCircuit` also still has two constructor methods that are legacy wrappers around + the importers in :mod:`qiskit.qasm2`. These automatically apply :ref:`the legacy compatibility + settings ` of :func:`~.qasm2.load` and :func:`~.qasm2.loads`. + + .. automethod:: from_qasm_file + .. automethod:: from_qasm_str + + Data objects on circuits + ======================== - * If a list of :class:`~.Register` objects, represents the :class:`.QuantumRegister` - and/or :class:`.ClassicalRegister` objects to include in the circuit. + .. _circuit-adding-data-objects: + + Adding data objects + ------------------- - For example: + ============================= ================================================================= + Method Adds this kind of data + ============================= ================================================================= + :meth:`add_bits` :class:`.Qubit`\\ s and :class:`.Clbit`\\ s. + :meth:`add_register` :class:`.QuantumRegister` and :class:`.ClassicalRegister`. + :meth:`add_var` :class:`~.expr.Var` nodes with local scope and initializers. + :meth:`add_input` :class:`~.expr.Var` nodes that are treated as circuit inputs. + :meth:`add_capture` :class:`~.expr.Var` nodes captured from containing scopes. + :meth:`add_uninitialized_var` :class:`~.expr.Var` nodes with local scope and undefined state. + ============================= ================================================================= - * ``QuantumCircuit(QuantumRegister(4))`` - * ``QuantumCircuit(QuantumRegister(4), ClassicalRegister(3))`` - * ``QuantumCircuit(QuantumRegister(4, 'qr0'), QuantumRegister(2, 'qr1'))`` + Typically you add most of the data objects (:class:`.Qubit`, :class:`.Clbit`, + :class:`.ClassicalRegister`, etc) to the circuit as part of using the :meth:`__init__` default + constructor, or :meth:`copy_empty_like`. However, it is also possible to add these afterwards. + Typed classical data, such as standalone :class:`~.expr.Var` nodes (see + :ref:`circuit-repr-real-time-classical`), can be both constructed and added with separate + methods. - * If a list of ``int``, the amount of qubits and/or classical bits to include in - the circuit. It can either be a single int for just the number of quantum bits, - or 2 ints for the number of quantum bits and classical bits, respectively. + New registerless :class:`.Qubit` and :class:`.Clbit` objects are added using :meth:`add_bits`. + These objects must not already be present in the circuit. You can check if a bit exists in the + circuit already using :meth:`find_bit`. + + .. automethod:: add_bits + + Registers are added to the circuit with :meth:`add_register`. In this method, it is not an + error if some of the bits are already present in the circuit. In this case, the register will + be an "alias" over the bits. This is not generally well-supported by hardware backends; it is + probably best to stay away from relying on it. The registers a given bit is in are part of the + return of :meth:`find_bit`. - For example: + .. automethod:: add_register - * ``QuantumCircuit(4) # A QuantumCircuit with 4 qubits`` - * ``QuantumCircuit(4, 3) # A QuantumCircuit with 4 qubits and 3 classical bits`` + :ref:`Real-time, typed classical data ` is represented on the + circuit by :class:`~.expr.Var` nodes with a well-defined :class:`~.types.Type`. It is possible + to instantiate these separately to a circuit (see :meth:`.Var.new`), but it is often more + convenient to use circuit methods that will automatically manage the types and expression + initialization for you. The two most common methods are :meth:`add_var` (locally scoped + variables) and :meth:`add_input` (inputs to the circuit). - * If a list of python lists containing :class:`.Bit` objects, a collection of - :class:`.Bit` s to be added to the circuit. + .. automethod:: add_var + .. automethod:: add_input + In addition, there are two lower-level methods that can be useful for programmatic generation of + circuits. When working interactively, you will most likely not need these; most uses of + :meth:`add_uninitialized_var` are part of :meth:`copy_empty_like`, and most uses of + :meth:`add_capture` would be better off using :ref:`the control-flow builder interface + `. - name (str): the name of the quantum circuit. If not set, an - automatically generated string will be assigned. - global_phase (float or ParameterExpression): The global phase of the circuit in radians. - metadata (dict): Arbitrary key value metadata to associate with the - circuit. This gets stored as free-form data in a dict in the - :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will - not be directly used in the circuit. - inputs: any variables to declare as ``input`` real-time variables for this circuit. These - should already be existing :class:`.expr.Var` nodes that you build from somewhere else; - if you need to create the inputs as well, use :meth:`QuantumCircuit.add_input`. The - variables given in this argument will be passed directly to :meth:`add_input`. A - circuit cannot have both ``inputs`` and ``captures``. - captures: any variables that that this circuit scope should capture from a containing scope. - The variables given here will be passed directly to :meth:`add_capture`. A circuit - cannot have both ``inputs`` and ``captures``. - declarations: any variables that this circuit should declare and initialize immediately. - You can order this input so that later declarations depend on earlier ones (including - inputs or captures). If you need to depend on values that will be computed later at - runtime, use :meth:`add_var` at an appropriate point in the circuit execution. + .. automethod:: add_uninitialized_var + .. automethod:: add_capture - This argument is intended for convenient circuit initialization when you already have a - set of created variables. The variables used here will be directly passed to - :meth:`add_var`, which you can use directly if this is the first time you are creating - the variable. + Working with bits and registers + ------------------------------- - Raises: - CircuitError: if the circuit name, if given, is not valid. - CircuitError: if both ``inputs`` and ``captures`` are given. + A :class:`.Bit` instance is, on its own, just a unique handle for circuits to use in their own + contexts. If you have got a :class:`.Bit` instance and a cirucit, just can find the contexts + that the bit exists in using :meth:`find_bit`, such as its integer index in the circuit and any + registers it is contained in. + + .. automethod:: find_bit + + Similarly, you can query a circuit to see if a register has already been added to it by using + :meth:`has_register`. + + .. automethod:: has_register + + Working with compile-time parameters + ------------------------------------ + + .. seealso:: + :ref:`circuit-compile-time-parameters` + A more complete discussion of what compile-time parametrization is, and how it fits into + Qiskit's data model. + + Unlike bits, registers, and real-time typed classical data, compile-time symbolic parameters are + not manually added to a circuit. Their presence is inferred by being contained in operations + added to circuits and the global phase. An ordered list of all parameters currently in a + circuit is at :attr:`QuantumCircuit.parameters`. + + The most common operation on :class:`.Parameter` instances is to replace them in symbolic + operations with some numeric value, or another symbolic expression. This is done with + :meth:`assign_parameters`. + + .. automethod:: assign_parameters + + The circuit tracks parameters by :class:`.Parameter` instances themselves, and forbids having + multiple parameters of the same name to avoid some problems when interoperating with OpenQASM or + other external formats. You can use :meth:`has_parameter` and :meth:`get_parameter` to query + the circuit for a parameter with the given string name. + + .. automethod:: has_parameter + .. automethod:: get_parameter + + .. _circuit-real-time-methods: + + Working with real-time typed classical data + ------------------------------------------- + + .. seealso:: + :mod:`qiskit.circuit.classical` + Module-level documentation for how the variable-, expression- and type-systems work, the + objects used to represent them, and the classical operations available. + + :ref:`circuit-repr-real-time-classical` + A discussion of how real-time data fits into the entire :mod:`qiskit.circuit` data model + as a whole. + + :ref:`circuit-adding-data-objects` + The methods for adding new :class:`~.expr.Var` variables to a circuit after + initialization. + + You can retrive a :class:`~.expr.Var` instance attached to a circuit by using its variable name + using :meth:`get_var`, or check if a circuit contains a given variable with :meth:`has_var`. + + .. automethod:: get_var + .. automethod:: has_var + + There are also several iterator methods that you can use to get the full set of variables + tracked by a circuit. At least one of :meth:`iter_input_vars` and :meth:`iter_captured_vars` + will be empty, as inputs and captures are mutually exclusive. All of the iterators have + corresponding dynamic properties on :class:`QuantumCircuit` that contain their length: + :attr:`num_vars`, :attr:`num_input_vars`, :attr:`num_captured_vars` and + :attr:`num_declared_vars`. + + .. automethod:: iter_vars + .. automethod:: iter_input_vars + .. automethod:: iter_captured_vars + .. automethod:: iter_declared_vars + + + .. _circuit-adding-operations: + + Adding operations to circuits + ============================= + + You can add anything that implements the :class:`.Operation` interface to a circuit as a single + instruction, though most things you will want to add will be :class:`~.circuit.Instruction` or + :class:`~.circuit.Gate` instances. + + .. seealso:: + :ref:`circuit-operations-instructions` + The :mod:`qiskit.circuit`-level documentation on the different interfaces that Qiskit + uses to define circuit-level instructions. + + .. _circuit-append-compose: + + Methods to add general operations + --------------------------------- + + These are the base methods that handle adding any object, including user-defined ones, onto + circuits. + + =============== =============================================================================== + Method When to use it + =============== =============================================================================== + :meth:`append` Add an instruction as a single object onto a circuit. + :meth:`_append` Same as :meth:`append`, but a low-level interface that elides almost all error + checking. + :meth:`compose` Inline the instructions from one circuit onto another. + :meth:`tensor` Like :meth:`compose`, but strictly for joining circuits that act on disjoint + qubits. + =============== =============================================================================== + + :class:`QuantumCircuit` has two main ways that you will add more operations onto a circuit. + Which to use depends on whether you want to add your object as a single "instruction" + (:meth:`append`), or whether you want to join the instructions from two circuits together + (:meth:`compose`). + + A single instruction or operation appears as a single entry in the :attr:`data` of the circuit, + and as a single box when drawn in the circuit visualizers (see :meth:`draw`). A single + instruction is the "unit" that a hardware backend might be defined in terms of (see + :class:`.Target`). An :class:`~.circuit.Instruction` can come with a + :attr:`~.circuit.Instruction.definition`, which is one rule the transpiler (see + :mod:`qiskit.transpiler`) will be able to fall back on to decompose it for hardware, if needed. + An :class:`.Operation` that is not also an :class:`~.circuit.Instruction` can + only be decomposed if it has some associated high-level synthesis method registered for it (see + :mod:`qiskit.transpiler.passes.synthesis.plugin`). + + A :class:`QuantumCircuit` alone is not a single :class:`~.circuit.Instruction`; it is rather + more complicated, since it can, in general, represent a complete program with typed classical + memory inputs and outputs, and control flow. Qiskit's (and most hardware's) data model does not + yet have the concept of re-usable callable subroutines with virtual quantum operands. You can + convert simple circuits that act only on qubits with unitary operations into a :class:`.Gate` + using :meth:`to_gate`, and simple circuits acting only on qubits and clbits into a + :class:`~.circuit.Instruction` with :meth:`to_instruction`. + + When you have an :class:`.Operation`, :class:`~.circuit.Instruction`, or :class:`.Gate`, add it + to the circuit, specifying the qubit and clbit arguments with :meth:`append`. + + .. automethod:: append + + :meth:`append` does quite substantial error checking to ensure that you cannot accidentally + break the data model of :class:`QuantumCircuit`. If you are programmatically generating a + circuit from known-good data, you can elide much of this error checking by using the fast-path + appender :meth:`_append`, but at the risk that the caller is responsible for ensuring they are + passing only valid data. + + .. automethod:: _append + + In other cases, you may want to join two circuits together, applying the instructions from one + circuit onto specified qubits and clbits on another circuit. This "inlining" operation is + called :meth:`compose` in Qiskit. :meth:`compose` is, in general, more powerful than + a :meth:`to_instruction`-plus-:meth:`append` combination for joining two circuits, because it + can also link typed classical data together, and allows for circuit control-flow operations to + be joined onto another circuit. + + The downsides to :meth:`compose` are that it is a more complex operation that can involve more + rewriting of the operand, and that it necessarily must move data from one circuit object to + another. If you are building up a circuit for yourself and raw performance is a core goal, + consider passing around your base circuit and having different parts of your algorithm write + directly to the base circuit, rather than building a temporary layer circuit. + + .. automethod:: compose + + If you are trying to join two circuits that will apply to completely disjoint qubits and clbits, + :meth:`tensor` is a convenient wrapper around manually adding bit objects and calling + :meth:`compose`. + + .. automethod:: tensor + + As some rules of thumb: + + * If you have a single :class:`.Operation`, :class:`~.circuit.Instruction` or :class:`.Gate`, + you should definitely use :meth:`append` or :meth:`_append`. + * If you have a :class:`QuantumCircuit` that represents a single atomic instruction for a larger + circuit that you want to re-use, you probably want to call :meth:`to_instruction` or + :meth:`to_gate`, and then apply the result of that to the circuit using :meth:`append`. + * If you have a :class:`QuantumCircuit` that represents a larger "layer" of another circuit, or + contains typed classical variables or control flow, you should use :meth:`compose` to merge it + onto another circuit. + * :meth:`tensor` is wanted far more rarely than either :meth:`append` or :meth:`compose`. + Internally, it is mostly a wrapper around :meth:`add_bits` and :meth:`compose`. + + Some potential pitfalls to beware of: + + * Even if you re-use a custom :class:`~.circuit.Instruction` during circuit construction, the + transpiler will generally have to "unroll" each invocation of it to its inner decomposition + before beginning work on it. This should not prevent you from using the + :meth:`to_instruction`-plus-:meth:`append` pattern, as the transpiler will improve in this + regard over time. + * :meth:`compose` will, by default, produce a new circuit for backwards compatibility. This is + more expensive, and not usually what you want, so you should set ``inplace=True``. + * Both :meth:`append` and :meth:`compose` (but not :meth:`_append`) have a ``copy`` keyword + argument that defaults to ``True``. In these cases, the incoming :class:`.Operation` + instances will be copied if Qiskit detects that the objects have mutability about them (such + as taking gate parameters). If you are sure that you will not re-use the objects again in + other places, you should set ``copy=False`` to prevent this copying, which can be a + substantial speed-up for large objects. + + Methods to add standard instructions + ------------------------------------ + + The :class:`QuantumCircuit` class has helper methods to add many of the Qiskit standard-library + instructions and gates onto a circuit. These are generally equivalent to manually constructing + an instance of the relevent :mod:`qiskit.circuit.library` object, then passing that to + :meth:`append` with the remaining arguments placed into the ``qargs`` and ``cargs`` fields as + appropriate. + + The following methods apply special non-unitary :class:`~.circuit.Instruction` operations to the + circuit: + + =============================== ==================================================== + :class:`QuantumCircuit` method :mod:`qiskit.circuit` :class:`~.circuit.Instruction` + =============================== ==================================================== + :meth:`barrier` :class:`Barrier` + :meth:`delay` :class:`Delay` + :meth:`initialize` :class:`~library.Initialize` + :meth:`measure` :class:`Measure` + :meth:`reset` :class:`Reset` + :meth:`store` :class:`Store` + =============================== ==================================================== + + These methods apply uncontrolled unitary :class:`.Gate` instances to the circuit: + + =============================== ============================================ + :class:`QuantumCircuit` method :mod:`qiskit.circuit.library` :class:`.Gate` + =============================== ============================================ + :meth:`dcx` :class:`~library.DCXGate` + :meth:`ecr` :class:`~library.ECRGate` + :meth:`h` :class:`~library.HGate` + :meth:`id` :class:`~library.IGate` + :meth:`iswap` :class:`~library.iSwapGate` + :meth:`ms` :class:`~library.MSGate` + :meth:`p` :class:`~library.PhaseGate` + :meth:`pauli` :class:`~library.PauliGate` + :meth:`prepare_state` :class:`~library.StatePreparation` + :meth:`r` :class:`~library.RGate` + :meth:`rcccx` :class:`~library.RC3XGate` + :meth:`rccx` :class:`~library.RCCXGate` + :meth:`rv` :class:`~library.RVGate` + :meth:`rx` :class:`~library.RXGate` + :meth:`rxx` :class:`~library.RXXGate` + :meth:`ry` :class:`~library.RYGate` + :meth:`ryy` :class:`~library.RYYGate` + :meth:`rz` :class:`~library.RZGate` + :meth:`rzx` :class:`~library.RZXGate` + :meth:`rzz` :class:`~library.RZZGate` + :meth:`s` :class:`~library.SGate` + :meth:`sdg` :class:`~library.SdgGate` + :meth:`swap` :class:`~library.SwapGate` + :meth:`sx` :class:`~library.SXGate` + :meth:`sxdg` :class:`~library.SXdgGate` + :meth:`t` :class:`~library.TGate` + :meth:`tdg` :class:`~library.TdgGate` + :meth:`u` :class:`~library.UGate` + :meth:`unitary` :class:`~library.UnitaryGate` + :meth:`x` :class:`~library.XGate` + :meth:`y` :class:`~library.YGate` + :meth:`z` :class:`~library.ZGate` + =============================== ============================================ + + The following methods apply :class:`Gate` instances that are also controlled gates, so are + direct subclasses of :class:`ControlledGate`: + + =============================== ====================================================== + :class:`QuantumCircuit` method :mod:`qiskit.circuit.library` :class:`.ControlledGate` + =============================== ====================================================== + :meth:`ccx` :class:`~library.CCXGate` + :meth:`ccz` :class:`~library.CCZGate` + :meth:`ch` :class:`~library.CHGate` + :meth:`cp` :class:`~library.CPhaseGate` + :meth:`crx` :class:`~library.CRXGate` + :meth:`cry` :class:`~library.CRYGate` + :meth:`crz` :class:`~library.CRZGate` + :meth:`cs` :class:`~library.CSGate` + :meth:`csdg` :class:`~library.CSdgGate` + :meth:`cswap` :class:`~library.CSwapGate` + :meth:`csx` :class:`~library.CSXGate` + :meth:`cu` :class:`~library.CUGate` + :meth:`cx` :class:`~library.CXGate` + :meth:`cy` :class:`~library.CYGate` + :meth:`cz` :class:`~library.CZGate` + =============================== ====================================================== + + Finally, these methods apply particular generalized multiply controlled gates to the circuit, + often with eager syntheses. They are listed in terms of the *base* gate they are controlling, + since their exact output is often a synthesised version of a gate. + + =============================== ================================================= + :class:`QuantumCircuit` method Base :mod:`qiskit.circuit.library` :class:`.Gate` + =============================== ================================================= + :meth:`mcp` :class:`~library.PhaseGate` + :meth:`mcrx` :class:`~library.RXGate` + :meth:`mcry` :class:`~library.RYGate` + :meth:`mcrz` :class:`~library.RZGate` + :meth:`mcx` :class:`~library.XGate` + =============================== ================================================= + + The rest of this section is the API listing of all the individual methods; the tables above are + summaries whose links will jump you to the correct place. + + .. automethod:: barrier + .. automethod:: ccx + .. automethod:: ccz + .. automethod:: ch + .. automethod:: cp + .. automethod:: crx + .. automethod:: cry + .. automethod:: crz + .. automethod:: cs + .. automethod:: csdg + .. automethod:: cswap + .. automethod:: csx + .. automethod:: cu + .. automethod:: cx + .. automethod:: cy + .. automethod:: cz + .. automethod:: dcx + .. automethod:: delay + .. automethod:: ecr + .. automethod:: h + .. automethod:: id + .. automethod:: initialize + .. automethod:: iswap + .. automethod:: mcp + .. automethod:: mcrx + .. automethod:: mcry + .. automethod:: mcrz + .. automethod:: mcx + .. automethod:: measure + .. automethod:: ms + .. automethod:: p + .. automethod:: pauli + .. automethod:: prepare_state + .. automethod:: r + .. automethod:: rcccx + .. automethod:: rccx + .. automethod:: reset + .. automethod:: rv + .. automethod:: rx + .. automethod:: rxx + .. automethod:: ry + .. automethod:: ryy + .. automethod:: rz + .. automethod:: rzx + .. automethod:: rzz + .. automethod:: s + .. automethod:: sdg + .. automethod:: store + .. automethod:: swap + .. automethod:: sx + .. automethod:: sxdg + .. automethod:: t + .. automethod:: tdg + .. automethod:: u + .. automethod:: unitary + .. automethod:: x + .. automethod:: y + .. automethod:: z + + + .. _circuit-control-flow-methods: + + Adding control flow to circuits + ------------------------------- + + .. seealso:: + :ref:`circuit-control-flow-repr` + + Discussion of how control-flow operations are represented in the whole :mod:`qiskit.circuit` + context. + + ============================== ================================================================ + :class:`QuantumCircuit` method Control-flow instruction + ============================== ================================================================ + :meth:`if_test` :class:`.IfElseOp` with only a ``True`` body. + :meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies. + :meth:`while_loop` :class:`.WhileLoopOp`. + :meth:`switch` :class:`.SwitchCaseOp`. + :meth:`for_loop` :class:`.ForLoopOp`. + :meth:`break_loop` :class:`.BreakLoopOp`. + :meth:`continue_loop` :class:`.ContinueLoopOp`. + ============================== ================================================================ + + :class:`QuantumCircuit` has corresponding methods for all of the control-flow operations that + are supported by Qiskit. These have two forms for calling them. The first is a very + straightfowards convenience wrapper that takes in the block bodies of the instructions as + :class:`QuantumCircuit` arguments, and simply constructs and appends the corresponding + :class:`.ControlFlowOp`. + + The second form, which we strongly recommend you use for constructing control flow, is called + *the builder interface*. Here, the methods take only the real-time discriminant of the + operation, and return `context managers + `__ that you enter using + ``with``. You can then use regular :class:`QuantumCircuit` methods within those blocks to build + up the control-flow bodies, and Qiskit will automatically track which of the data resources are + needed for the inner blocks, building the complete :class:`.ControlFlowOp` as you leave the + ``with`` statement. It is far simpler and less error-prone to build control flow + programmatically this way. + + .. + TODO: expand the examples of the builder interface. + + .. automethod:: break_loop + .. automethod:: continue_loop + .. automethod:: for_loop + .. automethod:: if_else + .. automethod:: if_test + .. automethod:: switch + .. automethod:: while_loop + + + Converting circuits to single objects + ------------------------------------- + + As discussed in :ref:`circuit-append-compose`, you can convert a circuit to either an + :class:`~.circuit.Instruction` or a :class:`.Gate` using two helper methods. + + .. automethod:: to_instruction + .. automethod:: to_gate + + + Helper mutation methods + ----------------------- + + There are two higher-level methods on :class:`QuantumCircuit` for appending measurements to the + end of a circuit. Note that by default, these also add an extra register. + + .. automethod:: measure_active + .. automethod:: measure_all + + There are two "subtractive" methods on :class:`QuantumCircuit` as well. This is not a use-case + that :class:`QuantumCircuit` is designed for; typically you should just look to use + :meth:`copy_empty_like` in place of :meth:`clear`, and run :meth:`remove_final_measurements` as + its transpiler-pass form :class:`.RemoveFinalMeasurements`. + + .. automethod:: clear + .. automethod:: remove_final_measurements + + .. _circuit-calibrations: + + Manual calibration of instructions + ---------------------------------- + + :class:`QuantumCircuit` can store :attr:`calibrations` of instructions that define the pulses + used to run them on one particular hardware backend. You can + + .. automethod:: add_calibration + .. automethod:: has_calibration_for + + + Circuit properties + ================== + + Simple circuit metrics + ---------------------- + + When constructing quantum circuits, there are several properties that help quantify + the "size" of the circuits, and their ability to be run on a noisy quantum device. + Some of these, like number of qubits, are straightforward to understand, while others + like depth and number of tensor components require a bit more explanation. Here we will + explain all of these properties, and, in preparation for understanding how circuits change + when run on actual devices, highlight the conditions under which they change. + + Consider the following circuit: + + .. plot:: + :include-source: + + from qiskit import QuantumCircuit + qc = QuantumCircuit(12) + for idx in range(5): + qc.h(idx) + qc.cx(idx, idx+5) + + qc.cx(1, 7) + qc.x(8) + qc.cx(1, 9) + qc.x(7) + qc.cx(1, 11) + qc.swap(6, 11) + qc.swap(6, 9) + qc.swap(6, 10) + qc.x(6) + qc.draw('mpl') + + From the plot, it is easy to see that this circuit has 12 qubits, and a collection of + Hadamard, CNOT, X, and SWAP gates. But how to quantify this programmatically? Because we + can do single-qubit gates on all the qubits simultaneously, the number of qubits in this + circuit is equal to the :meth:`width` of the circuit:: + + assert qc.width() == 12 + + We can also just get the number of qubits directly using :attr:`num_qubits`:: + + assert qc.num_qubits == 12 + + .. important:: + + For a quantum circuit composed from just qubits, the circuit width is equal + to the number of qubits. This is the definition used in quantum computing. However, + for more complicated circuits with classical registers, and classically controlled gates, + this equivalence breaks down. As such, from now on we will not refer to the number of + qubits in a quantum circuit as the width. + + It is also straightforward to get the number and type of the gates in a circuit using + :meth:`count_ops`:: + + qc.count_ops() - Examples: + .. parsed-literal:: - Construct a simple Bell state circuit. + OrderedDict([('cx', 8), ('h', 5), ('x', 3), ('swap', 3)]) - .. plot:: - :include-source: + We can also get just the raw count of operations by computing the circuits + :meth:`size`:: - from qiskit import QuantumCircuit + assert qc.size() == 19 - qc = QuantumCircuit(2, 2) - qc.h(0) - qc.cx(0, 1) - qc.measure([0, 1], [0, 1]) - qc.draw('mpl') + A particularly important circuit property is known as the circuit :meth:`depth`. The depth + of a quantum circuit is a measure of how many "layers" of quantum gates, executed in + parallel, it takes to complete the computation defined by the circuit. Because quantum + gates take time to implement, the depth of a circuit roughly corresponds to the amount of + time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit + is one important quantity used to measure if a quantum circuit can be run on a device. - Construct a 5-qubit GHZ circuit. + The depth of a quantum circuit has a mathematical definition as the longest path in a + directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for + experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar + with playing `Tetris `_. Lets see how to compute this + graphically: - .. code-block:: + .. image:: /source_images/depth.gif - from qiskit import QuantumCircuit + We can verify our graphical result using :meth:`QuantumCircuit.depth`:: - qc = QuantumCircuit(5) - qc.h(0) - qc.cx(0, range(1, 5)) - qc.measure_all() + assert qc.depth() == 9 - Construct a 4-qubit Bernstein-Vazirani circuit using registers. + .. automethod:: count_ops + .. automethod:: depth + .. automethod:: get_instructions + .. automethod:: num_connected_components + .. automethod:: num_nonlocal_gates + .. automethod:: num_tensor_factors + .. automethod:: num_unitary_factors + .. automethod:: size + .. automethod:: width - .. plot:: - :include-source: + Accessing scheduling information + -------------------------------- - from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit + If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing + information for individual qubits can be accessed. The whole-circuit timing information is + available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes. + + .. automethod:: qubit_duration + .. automethod:: qubit_start_time + .. automethod:: qubit_stop_time + + Instruction-like methods + ======================== - qr = QuantumRegister(3, 'q') - anc = QuantumRegister(1, 'ancilla') - cr = ClassicalRegister(3, 'c') - qc = QuantumCircuit(qr, anc, cr) + .. + These methods really shouldn't be on `QuantumCircuit` at all. They're generally more + appropriate as `Instruction` or `Gate` methods. `reverse_ops` shouldn't be a method _full + stop_---it was copying a `DAGCircuit` method from an implementation detail of the original + `SabreLayout` pass in Qiskit. + + :class:`QuantumCircuit` also contains a small number of methods that are very + :class:`~.circuit.Instruction`-like in detail. You may well find better integration and more + API support if you first convert your circuit to an :class:`~.circuit.Instruction` + (:meth:`to_instruction`) or :class:`.Gate` (:meth:`to_gate`) as appropriate, then call the + corresponding method. - qc.x(anc[0]) - qc.h(anc[0]) - qc.h(qr[0:3]) - qc.cx(qr[0:3], anc[0]) - qc.h(qr[0:3]) - qc.barrier(qr) - qc.measure(qr, cr) + .. automethod:: control + .. automethod:: inverse + .. automethod:: power + .. automethod:: repeat + .. automethod:: reverse_ops - qc.draw('mpl') + Visualization + ============= + + Qiskit includes some drawing tools to give you a quick feel for what your circuit looks like. + This tooling is primarily targeted at producing either a `Matplotlib + `__- or text-based drawing. There is also a lesser-featured LaTeX + backend for drawing, but this is only for simple circuits, and is not as actively maintained. + + .. seealso:: + :mod:`qiskit.visualization` + The primary documentation for all of Qiskit's visualization tooling. + + .. automethod:: draw + + In addition to the core :meth:`draw` driver, there are two visualization-related helper methods, + which are mostly useful for quickly unwrapping some inner instructions or reversing the + :ref:`qubit-labelling conventions ` in the drawing. For more general + mutation, including basis-gate rewriting, you should use the transpiler + (:mod:`qiskit.transpiler`). + + .. automethod:: decompose + .. automethod:: reverse_bits + + Internal utilities + ================== + + These functions are not intended for public use, but were accidentally left documented in the + public API during the 1.0 release. They will be removed in Qiskit 2.0, but will be supported + until then. + + .. automethod:: cast + .. automethod:: cbit_argument_conversion + .. automethod:: cls_instances + .. automethod:: cls_prefix + .. automethod:: qbit_argument_conversion """ instances = 0 @@ -228,6 +993,69 @@ def __init__( captures: Iterable[expr.Var] = (), declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): + """ + Default constructor of :class:`QuantumCircuit`. + + .. + `QuantumCirucit` documents its `__init__` method explicitly, unlike most classes where + it's implicitly appended to the class-level documentation, just because the class is so + huge and has a lot of introductory material to its class docstring. + + Args: + regs: The registers to be included in the circuit. + + * If a list of :class:`~.Register` objects, represents the :class:`.QuantumRegister` + and/or :class:`.ClassicalRegister` objects to include in the circuit. + + For example: + + * ``QuantumCircuit(QuantumRegister(4))`` + * ``QuantumCircuit(QuantumRegister(4), ClassicalRegister(3))`` + * ``QuantumCircuit(QuantumRegister(4, 'qr0'), QuantumRegister(2, 'qr1'))`` + + * If a list of ``int``, the amount of qubits and/or classical bits to include in + the circuit. It can either be a single int for just the number of quantum bits, + or 2 ints for the number of quantum bits and classical bits, respectively. + + For example: + + * ``QuantumCircuit(4) # A QuantumCircuit with 4 qubits`` + * ``QuantumCircuit(4, 3) # A QuantumCircuit with 4 qubits and 3 classical bits`` + + * If a list of python lists containing :class:`.Bit` objects, a collection of + :class:`.Bit` s to be added to the circuit. + + name: the name of the quantum circuit. If not set, an automatically generated string + will be assigned. + global_phase: The global phase of the circuit in radians. + metadata: Arbitrary key value metadata to associate with the circuit. This gets + stored as free-form data in a dict in the + :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will not be directly + used in the circuit. + inputs: any variables to declare as ``input`` runtime variables for this circuit. These + should already be existing :class:`.expr.Var` nodes that you build from somewhere + else; if you need to create the inputs as well, use + :meth:`QuantumCircuit.add_input`. The variables given in this argument will be + passed directly to :meth:`add_input`. A circuit cannot have both ``inputs`` and + ``captures``. + captures: any variables that that this circuit scope should capture from a containing + scope. The variables given here will be passed directly to :meth:`add_capture`. A + circuit cannot have both ``inputs`` and ``captures``. + declarations: any variables that this circuit should declare and initialize immediately. + You can order this input so that later declarations depend on earlier ones + (including inputs or captures). If you need to depend on values that will be + computed later at runtime, use :meth:`add_var` at an appropriate point in the + circuit execution. + + This argument is intended for convenient circuit initialization when you already + have a set of created variables. The variables used here will be directly passed to + :meth:`add_var`, which you can use directly if this is the first time you are + creating the variable. + + Raises: + CircuitError: if the circuit name, if given, is not valid. + CircuitError: if both ``inputs`` and ``captures`` are given. + """ if any(not isinstance(reg, (list, QuantumRegister, ClassicalRegister)) for reg in regs): # check if inputs are integers, but also allow e.g. 2.0 @@ -244,6 +1072,8 @@ def __init__( regs = tuple(int(reg) for reg in regs) # cast to int self._base_name = None + self.name: str + """A human-readable name for the circuit.""" if name is None: self._base_name = self.cls_prefix() self._name_update() @@ -273,7 +1103,11 @@ def __init__( ] = [] self.qregs: list[QuantumRegister] = [] + """A list of the :class:`QuantumRegister`\\ s in this circuit. You should not mutate + this.""" self.cregs: list[ClassicalRegister] = [] + """A list of the :class:`ClassicalRegister`\\ s in this circuit. You should not mutate + this.""" # Dict mapping Qubit or Clbit instances to tuple comprised of 0) the # corresponding index in circuit.{qubits,clbits} and 1) a list of @@ -314,9 +1148,16 @@ def __init__( for var, initial in declarations: self.add_var(var, initial) - self.duration = None + self.duration: int | float | None = None + """The total duration of the circuit, set by a scheduling transpiler pass. Its unit is + specified by :attr:`unit`.""" self.unit = "dt" + """The unit that :attr:`duration` is specified in.""" self.metadata = {} if metadata is None else metadata + """Arbitrary user-defined metadata for the circuit. + + Qiskit will not examine the content of this mapping, but it will pass it through the + transpiler and reattach it to the output, so you can track your own metadata.""" @staticmethod def from_instructions( @@ -333,7 +1174,7 @@ def from_instructions( global_phase: ParameterValueType = 0, metadata: dict | None = None, ) -> "QuantumCircuit": - """Construct a circuit from an iterable of CircuitInstructions. + """Construct a circuit from an iterable of :class:`.CircuitInstruction`\\ s. Args: instructions: The instructions to add to the circuit. @@ -390,7 +1231,7 @@ def layout(self) -> Optional[TranspileLayout]: @property def data(self) -> QuantumCircuitData: - """Return the circuit data (instructions and context). + """The circuit data (instructions and context). Returns: QuantumCircuitData: a list-like object containing the :class:`.CircuitInstruction`\\ s @@ -884,9 +1725,13 @@ def compose( var_remap: Mapping[str | expr.Var, str | expr.Var] | None = None, inline_captures: bool = False, ) -> Optional["QuantumCircuit"]: - """Compose circuit with ``other`` circuit or instruction, optionally permuting wires. + """Apply the instructions from one circuit onto specified qubits and/or clbits on another. + + .. note:: - ``other`` can be narrower or of equal width to ``self``. + By default, this creates a new circuit object, leaving ``self`` untouched. For most + uses of this function, it is far more efficient to set ``inplace=True`` and modify the + base circuit in-place. When dealing with realtime variables (:class:`.expr.Var` instances), there are two principal strategies for using :meth:`compose`: @@ -915,8 +1760,6 @@ def compose( front (bool): If True, front composition will be performed. This is not possible within control-flow builder context managers. inplace (bool): If True, modify the object. Otherwise return composed circuit. - wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on - whether it contains only unitary instructions) before composing it onto self. copy (bool): If ``True`` (the default), then the input is treated as shared, and any contained instructions will be copied, if they might need to be mutated in the future. You can set this to ``False`` if the input should be considered owned by @@ -941,6 +1784,11 @@ def compose( If this is ``False`` (the default), then all variables in ``other`` will be required to be distinct from those in ``self``, and new declarations will be made for them. + wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on + whether it contains only unitary instructions) before composing it onto self. + Rather than using this option, it is almost always better to manually control this + yourself by using :meth:`to_instruction` or :meth:`to_gate`, and then call + :meth:`append`. Returns: QuantumCircuit: the composed circuit (returns None if inplace==True). @@ -1294,23 +2142,20 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu @property def qubits(self) -> list[Qubit]: - """ - Returns a list of quantum bits in the order that the registers were added. - """ + """A list of :class:`Qubit`\\ s in the order that they were added. You should not mutate + this.""" return self._data.qubits @property def clbits(self) -> list[Clbit]: - """ - Returns a list of classical bits in the order that the registers were added. - """ + """A list of :class:`Clbit`\\ s in the order that they were added. You should not mutate + this.""" return self._data.clbits @property def ancillas(self) -> list[AncillaQubit]: - """ - Returns a list of ancilla bits in the order that the registers were added. - """ + """A list of :class:`AncillaQubit`\\ s in the order that they were added. You should not + mutate this.""" return self._ancillas @property @@ -1557,33 +2402,31 @@ def append( # Preferred new style. @typing.overload - def _append( - self, instruction: CircuitInstruction, _qargs: None = None, _cargs: None = None - ) -> CircuitInstruction: ... + def _append(self, instruction: CircuitInstruction) -> CircuitInstruction: ... # To-be-deprecated old style. @typing.overload def _append( self, - operation: Operation, + instruction: Operation, qargs: Sequence[Qubit], cargs: Sequence[Clbit], ) -> Operation: ... - def _append( - self, - instruction: CircuitInstruction | Instruction, - qargs: Sequence[Qubit] | None = None, - cargs: Sequence[Clbit] | None = None, - ): + def _append(self, instruction, qargs=(), cargs=()): """Append an instruction to the end of the circuit, modifying the circuit in place. .. warning:: This is an internal fast-path function, and it is the responsibility of the caller to ensure that all the arguments are valid; there is no error checking here. In - particular, all the qubits and clbits must already exist in the circuit and there can be - no duplicates in the list. + particular: + + * all the qubits and clbits must already exist in the circuit and there can be no + duplicates in the list. + * any control-flow operations or classically conditioned instructions must act only on + variables present in the circuit. + * the circuit must not be within a control-flow builder context. .. note:: @@ -1596,12 +2439,18 @@ def _append( constructs of the control-flow builder interface. Args: - instruction: Operation instance to append - qargs: Qubits to attach the instruction to. - cargs: Clbits to attach the instruction to. + instruction: A complete well-formed :class:`.CircuitInstruction` of the operation and + its context to be added. + + In the legacy compatibility form, this can be a bare :class:`.Operation`, in which + case ``qargs`` and ``cargs`` must be explicitly given. + qargs: Legacy argument for qubits to attach the bare :class:`.Operation` to. Ignored if + the first argument is in the preferential :class:`.CircuitInstruction` form. + cargs: Legacy argument for clbits to attach the bare :class:`.Operation` to. Ignored if + the first argument is in the preferential :class:`.CircuitInstruction` form. Returns: - Operation: a handle to the instruction that was just added + CircuitInstruction: a handle to the instruction that was just added. :meta public: """ @@ -2114,24 +2963,52 @@ def add_bits(self, bits: Iterable[Bit]) -> None: def find_bit(self, bit: Bit) -> BitLocations: """Find locations in the circuit which can be used to reference a given :obj:`~Bit`. + In particular, this function can find the integer index of a qubit, which corresponds to its + hardware index for a transpiled circuit. + + .. note:: + The circuit index of a :class:`.AncillaQubit` will be its index in :attr:`qubits`, not + :attr:`ancillas`. + Args: bit (Bit): The bit to locate. Returns: namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) - contains the index at which the ``Bit`` can be found (in either - :obj:`~QuantumCircuit.qubits`, :obj:`~QuantumCircuit.clbits`, depending on its - type). The second element (``registers``) is a list of ``(register, index)`` - pairs with an entry for each :obj:`~Register` in the circuit which contains the - :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). - - Notes: - The circuit index of an :obj:`~AncillaQubit` will be its index in - :obj:`~QuantumCircuit.qubits`, not :obj:`~QuantumCircuit.ancillas`. + contains the index at which the ``Bit`` can be found (in either + :obj:`~QuantumCircuit.qubits`, :obj:`~QuantumCircuit.clbits`, depending on its + type). The second element (``registers``) is a list of ``(register, index)`` + pairs with an entry for each :obj:`~Register` in the circuit which contains the + :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). Raises: CircuitError: If the supplied :obj:`~Bit` was of an unknown type. CircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. + + Examples: + Loop through a circuit, getting the qubit and clbit indices of each operation:: + + from qiskit.circuit import QuantumCircuit, Qubit + + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.measure([0, 1, 2], [0, 1, 2]) + + # The `.qubits` and `.clbits` fields are not integers. + assert isinstance(qc.data[0].qubits[0], Qubit) + # ... but we can use `find_bit` to retrieve them. + assert qc.find_bit(qc.data[0].qubits[0]).index == 0 + + simple = [ + ( + instruction.operation.name, + [qc.find_bit(bit).index for bit in instruction.qubits], + [qc.find_bit(bit).index for bit in instruction.clbits], + ) + for instruction in qc.data + ] """ try: @@ -2157,18 +3034,22 @@ def to_instruction( parameter_map: dict[Parameter, ParameterValueType] | None = None, label: str | None = None, ) -> Instruction: - """Create an Instruction out of this circuit. + """Create an :class:`~.circuit.Instruction` out of this circuit. + + .. seealso:: + :func:`circuit_to_instruction` + The underlying driver of this method. Args: - parameter_map(dict): For parameterized circuits, a mapping from + parameter_map: For parameterized circuits, a mapping from parameters in the circuit to parameters to be used in the instruction. If None, existing circuit parameters will also parameterize the instruction. - label (str): Optional gate label. + label: Optional gate label. Returns: - qiskit.circuit.Instruction: a composite instruction encapsulating this circuit - (can be decomposed back) + qiskit.circuit.Instruction: a composite instruction encapsulating this circuit (can be + decomposed back). """ from qiskit.converters.circuit_to_instruction import circuit_to_instruction @@ -2179,18 +3060,21 @@ def to_gate( parameter_map: dict[Parameter, ParameterValueType] | None = None, label: str | None = None, ) -> Gate: - """Create a Gate out of this circuit. + """Create a :class:`.Gate` out of this circuit. The circuit must act only qubits and + contain only unitary operations. + + .. seealso:: + :func:`circuit_to_gate` + The underlying driver of this method. Args: - parameter_map(dict): For parameterized circuits, a mapping from - parameters in the circuit to parameters to be used in the - gate. If None, existing circuit parameters will also - parameterize the gate. - label (str): Optional gate label. + parameter_map: For parameterized circuits, a mapping from parameters in the circuit to + parameters to be used in the gate. If ``None``, existing circuit parameters will + also parameterize the gate. + label : Optional gate label. Returns: - Gate: a composite gate encapsulating this circuit - (can be decomposed back) + Gate: a composite gate encapsulating this circuit (can be decomposed back). """ from qiskit.converters.circuit_to_gate import circuit_to_gate @@ -2417,25 +3301,36 @@ def size( def depth( self, - filter_function: Callable[..., int] = lambda x: not getattr( + filter_function: Callable[[CircuitInstruction], bool] = lambda x: not getattr( x.operation, "_directive", False ), ) -> int: """Return circuit depth (i.e., length of critical path). Args: - filter_function (callable): A function to filter instructions. - Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)). - Instructions for which the function returns False are ignored in the - computation of the circuit depth. - By default filters out "directives", such as barrier or snapshot. + filter_function: A function to decide which instructions count to increase depth. + Should take as a single positional input a :class:`CircuitInstruction`. + Instructions for which the function returns ``False`` are ignored in the + computation of the circuit depth. By default filters out "directives", such as + :class:`.Barrier`. Returns: int: Depth of circuit. - Notes: - The circuit depth and the DAG depth need not be the - same. + Examples: + Simple calculation of total circuit depth:: + + from qiskit.circuit import QuantumCircuit + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.h(2) + qc.cx(2, 3) + assert qc.depth() == 2 + + Modifying the previous example to only calculate the depth of multi-qubit gates:: + + assert qc.depth(lambda instr: len(instr.qubits) > 1) == 1 """ # Assign each bit in the circuit a unique integer # to index into op_stack. @@ -2773,6 +3668,11 @@ def clear(self) -> None: """Clear all instructions in self. Clearing the circuits will keep the metadata and calibrations. + + .. seealso:: + :meth:`copy_empty_like` + A method to produce a new circuit with no instructions and all the same tracking of + quantum and classical typed data, but without mutating the original circuit. """ self._data.clear() self._parameter_table.clear() @@ -3007,6 +3907,28 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi Measurements and barriers are considered final if they are followed by no other operations (aside from other measurements or barriers.) + .. note:: + This method has rather complex behavior, particularly around the removal of newly idle + classical bits and registers. It is much more efficient to avoid adding unnecessary + classical data in the first place, rather than trying to remove it later. + + .. seealso:: + :class:`.RemoveFinalMeasurements` + A transpiler pass that removes final measurements and barriers. This does not + remove the classical data. If this is your goal, you can call that with:: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import RemoveFinalMeasurements + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.barrier() + qc.measure([0, 1], [0, 1]) + + pass_ = RemoveFinalMeasurements() + just_bell = pass_(qc) + Args: inplace (bool): All measurements removed inplace or return new circuit. @@ -3110,7 +4032,7 @@ def from_qasm_str(qasm_str: str) -> "QuantumCircuit": @property def global_phase(self) -> ParameterValueType: - """Return the global phase of the current circuit scope in radians.""" + """The global phase of the current circuit scope in radians.""" if self._control_flow_scopes: return self._control_flow_scopes[-1].global_phase return self._global_phase @@ -5206,15 +6128,7 @@ def for_loop( ) @typing.overload - def if_test( - self, - condition: tuple[ClassicalRegister | Clbit, int], - true_body: None, - qubits: None, - clbits: None, - *, - label: str | None, - ) -> IfContext: ... + def if_test(self, condition: tuple[ClassicalRegister | Clbit, int]) -> IfContext: ... @typing.overload def if_test( From 554e661ee62ba6db1cfcbef0fabd98a1659a1641 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 16 May 2024 18:31:55 +0100 Subject: [PATCH 072/159] Add release notes for manual `Var` and `Store` (#12421) * Add release notes for manual `Var` and `Store` This adds the release notes and updates some small portions of documentation that were previously missed surrounding the new manual `Var` storage locations. This includes documenting all new keyword arguments to methods, upgrade instructions for providers, and adding the `Var.new` method to the documentation, which was previously erroneously omitted. * Fix Sphinx typo * Fix another Sphinx typo * Move QPY version bump to upgrade * Unify base release note * Reword providers upgrade note Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/circuit/classical/expr/__init__.py | 2 +- qiskit/providers/__init__.py | 39 +++++- .../1.1/classical-store-e64ee1286219a862.yaml | 13 ++ .../notes/storage-var-a00a33fcf9a71f3f.yaml | 122 ++++++++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index c0057ca96f0..00f1c2e0676 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -43,7 +43,7 @@ real-time variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. .. autoclass:: Var - :members: var, name + :members: var, name, new Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes with associated types. diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index b0ebc942523..6736d67a214 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -452,8 +452,45 @@ def get_translation_stage_plugin(self): efficient output on ``Mybackend`` the transpiler will be able to perform these custom steps without any manual user input. +.. _providers-guide-real-time-variables: + +Real-time variables +^^^^^^^^^^^^^^^^^^^ + +The transpiler will automatically handle real-time typed classical variables (see +:mod:`qiskit.circuit.classical`) and treat the :class:`.Store` instruction as a built-in +"directive", similar to :class:`.Barrier`. No special handling from backends is necessary to permit +this. + +If your backend is *unable* to handle classical variables and storage, we recommend that you comment +on this in your documentation, and insert a check into your :meth:`~.BackendV2.run` method (see +:ref:`providers-guide-backend-run`) to eagerly reject circuits containing them. You can examine +:attr:`.QuantumCircuit.num_vars` for the presence of variables at the top level. If you accept +:ref:`control-flow operations `, you might need to recursively search the +internal :attr:`~.ControlFlowOp.blocks` of each for scope-local variables with +:attr:`.QuantumCircuit.num_declared_vars`. + +For example, a function to check for the presence of any manual storage locations, or manual stores +to memory:: + + from qiskit.circuit import Store, ControlFlowOp, QuantumCircuit + + def has_realtime_logic(circuit: QuantumCircuit) -> bool: + if circuit.num_vars: + return True + for instruction in circuit.data: + if isinstance(instruction.operation, Store): + return True + elif isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + if has_realtime_logic(block): + return True + return False + +.. _providers-guide-backend-run: + Backend.run Method --------------------- +------------------ Of key importance is the :meth:`~qiskit.providers.BackendV2.run` method, which is used to actually submit circuits to a device or simulator. The run method diff --git a/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml b/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml index 9de8affebe4..6718cd66f1f 100644 --- a/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml +++ b/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml @@ -54,3 +54,16 @@ features_circuits: Variables can be used wherever classical expressions (see :mod:`qiskit.circuit.classical.expr`) are valid. Currently this is the target expressions of control-flow operations, though we plan to expand this to gate parameters in the future, as the type and expression system are expanded. + + See :ref:`circuit-repr-real-time-classical` for more discussion of these variables, and the + associated data model. + + These are supported throughout the transpiler, through QPY serialization (:mod:`qiskit.qpy`), + OpenQASM 3 export (:mod:`qiskit.qasm3`), and have initial support through the circuit visualizers + (see :meth:`.QuantumCircuit.draw`). + + .. note:: + + The new classical variables and storage will take some time to become supported on hardware + and simulator backends. They are not supported in the primitives interfaces + (:mod:`qiskit.primitives`), but will likely inform those interfaces as they evolve. diff --git a/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml b/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml new file mode 100644 index 00000000000..b3b18be2fc1 --- /dev/null +++ b/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml @@ -0,0 +1,122 @@ +--- +features_circuits: + - | + :class:`.QuantumCircuit` has several new methods to work with and inspect manual :class:`.Var` + variables. + + See :ref:`circuit-real-time-methods` for more in-depth discussion on all of these. + + The new methods are: + + * :meth:`~.QuantumCircuit.add_var` + * :meth:`~.QuantumCircuit.add_input` + * :meth:`~.QuantumCircuit.add_capture` + * :meth:`~.QuantumCircuit.add_uninitialized_var` + * :meth:`~.QuantumCircuit.get_var` + * :meth:`~.QuantumCircuit.has_var` + * :meth:`~.QuantumCircuit.iter_vars` + * :meth:`~.QuantumCircuit.iter_declared_vars` + * :meth:`~.QuantumCircuit.iter_captured_vars` + * :meth:`~.QuantumCircuit.iter_input_vars` + * :meth:`~.QuantumCircuit.store` + + In addition, there are several new dynamic attributes on :class:`.QuantumCircuit` surrounding + these variables: + + * :attr:`~.QuantumCircuit.num_vars` + * :attr:`~.QuantumCircuit.num_input_vars` + * :attr:`~.QuantumCircuit.num_captured_vars` + * :attr:`~.QuantumCircuit.num_declared_vars` + - | + :class:`.ControlFlowOp` and its subclasses now have a :meth:`~.ControlFlowOp.iter_captured_vars` + method, which will return an iterator over the unique variables captured in any of its immediate + blocks. + - | + :class:`.DAGCircuit` has several new methods to work with and inspect manual :class:`.Var` + variables. These are largely equivalent to their :class:`.QuantumCircuit` counterparts, except + that the :class:`.DAGCircuit` ones are optimized for programmatic access with already defined + objects, while the :class:`.QuantumCircuit` methods are more focussed on interactive human use. + + The new methods are: + + * :meth:`~.DAGCircuit.add_input_var` + * :meth:`~.DAGCircuit.add_captured_var` + * :meth:`~.DAGCircuit.add_declared_var` + * :meth:`~.DAGCircuit.has_var` + * :meth:`~.DAGCircuit.iter_vars` + * :meth:`~.DAGCircuit.iter_declared_vars` + * :meth:`~.DAGCircuit.iter_captured_vars` + * :meth:`~.DAGCircuit.iter_input_vars` + + There are also new public attributes: + + * :attr:`~.DAGCircuit.num_vars` + * :attr:`~.DAGCircuit.num_input_vars` + * :attr:`~.DAGCircuit.num_captured_vars` + * :attr:`~.DAGCircuit.num_declared_vars` + - | + :attr:`.DAGCircuit.wires` will now also contain any :class:`.Var` manual variables in the + circuit as well, as these are also classical data flow. + - | + A new method, :meth:`.Var.new`, is added to manually construct a real-time classical variable + that owns its memory. + - | + :meth:`.QuantumCircuit.compose` has two need keyword arguments, ``var_remap`` and ``inline_captures`` + to better support real-time classical variables. + + ``var_remap`` can be used to rewrite :class:`.Var` nodes in the circuit argument as its + instructions are inlined onto the base circuit. This can be used to avoid naming conflicts. + + ``inline_captures`` can be set to ``True`` (defaults to ``False``) to link all :class:`.Var` + nodes tracked as "captures" in the argument circuit with the same :class:`.Var` nodes in the + base circuit, without attempting to redeclare the variables. This can be used, in combination + with :meth:`.QuantumCircuit.copy_empty_like`'s ``vars_mode="captures"`` handling, to build up + a circuit layer by layer, containing variables. + - | + :meth:`.DAGCircuit.compose` has a new keyword argument, ``inline_captures``, which can be set to + ``True`` to inline "captured" :class:`.Var` nodes on the argument circuit onto the base circuit + without redeclaring them. In conjunction with the ``vars_mode="captures"`` option to several + :class:`.DAGCircuit` methods, this can be used to combine DAGs that operate on the same variables. + - | + :meth:`.QuantumCircuit.copy_empty_like` and :meth:`.DAGCircuit.copy_empty_like` have a new + keyword argument, ``vars_mode`` which controls how any memory-owning :class:`.Var` nodes are + tracked in the output. By default (``"alike"``), the variables are declared in the same + input/captured/local mode as the source. This can be set to ``"captures"`` to convert all + variables to captures (useful with :meth:`~.QuantumCircuit.compose`) or ``"drop"`` to remove + them. + - | + A new ``vars_mode`` keyword argument has been added to the :class:`.DAGCircuit` methods: + + * :meth:`~.DAGCircuit.separable_circuits` + * :meth:`~.DAGCircuit.layers` + * :meth:`~.DAGCircuit.serial_layers` + + which has the same meaning as it does for :meth:`~.DAGCircuit.copy_empty_like`. +features_qasm: + - | + The OpenQASM 3 exporter supports manual-storage :class:`.Var` nodes on circuits. +features_qpy: + - | + QPY (:mod:`qiskit.qpy`) format version 12 has been added, which includes support for memory-owning + :class:`.Var` variables. See :ref:`qpy_version_12` for more detail on the format changes. +features_visualization: + - | + The text and `Matplotlib `__ circuit drawers (:meth:`.QuantumCircuit.draw`) + have minimal support for displaying expressions involving manual real-time variables. The + :class:`.Store` operation and the variable initializations are not yet supported; for large-scale + dynamic circuits, we recommend using the OpenQASM 3 export capabilities (:func:`.qasm3.dumps`) to + get a textual representation of a circuit. +upgrade_qpy: + - | + The value of :attr:`qiskit.qpy.QPY_VERSION` is now 12. :attr:`.QPY_COMPATIBILITY_VERSION` is + unchanged at 10. +upgrade_providers: + - | + Implementations of :class:`.BackendV2` (and :class:`.BackendV1`) may desire to update their + :meth:`~.BackendV2.run` methods to eagerly reject inputs containing typed + classical variables (see :mod:`qiskit.circuit.classical`) and the :class:`.Store` instruction, + if they do not have support for them. The new :class:`.Store` instruction is treated by the + transpiler as an always-available "directive" (like :class:`.Barrier`); if your backends do not + support this won't be caught by the :mod:`~qiskit.transpiler`. + + See :ref:`providers-guide-real-time-variables` for more information. From 8b569959d046614e338e100cb8033e5918e778eb Mon Sep 17 00:00:00 2001 From: Arnau Casau <47946624+arnaucasau@users.noreply.github.com> Date: Fri, 17 May 2024 02:58:50 +0200 Subject: [PATCH 073/159] Fix `qiskit.circuit` method header and broken cross-reference (#12394) * Fix qiskit.circuit method header * use object * fix lint * Correct method to be defined on `object` --------- Co-authored-by: Jake Lishman --- qiskit/circuit/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 3982fa87334..43087760153 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -816,10 +816,11 @@ ``__array__``. This is used by :meth:`Gate.to_matrix`, and has the signature: .. currentmodule:: None -.. py:method:: __array__(dtype=None, copy=None) +.. py:method:: object.__array__(dtype=None, copy=None) - Return a Numpy array representing the gate. This can use the gate's :attr:`~Instruction.params` - field, and may assume that these are numeric values (assuming the subclass expects that) and not + Return a Numpy array representing the gate. This can use the gate's + :attr:`~qiskit.circuit.Instruction.params` field, and may assume that these are numeric + values (assuming the subclass expects that) and not :ref:`compile-time parameters `. For greatest efficiency, the returned array should default to a dtype of :class:`complex`. From 8571afe3775fdd2f72354142d14bfc2ac0292da6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 May 2024 08:59:31 -0400 Subject: [PATCH 074/159] Add merge queue to required tests on github actions (#12428) This commit adds the missing config to the github actions workflow for running required tests (currently only arm64 macOS test jobs) to the merge queue. This is necessary to make the job required in the branch protection rules, because the jobs will need to pass as part of the merge queue too. --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0d04e21a169..08530adfd4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: branches: [ main, 'stable/*' ] pull_request: branches: [ main, 'stable/*' ] + merge_group: concurrency: group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} From 4e3de44bcbde61fae33848a94be2622f5f5bd959 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Fri, 17 May 2024 12:55:48 -0400 Subject: [PATCH 075/159] Add missing paranthesis to pauli_feature_map.py (#12434) --- qiskit/circuit/library/data_preparation/pauli_feature_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index b05287ca049..03bbc031ec6 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -97,7 +97,7 @@ class PauliFeatureMap(NLocal): >>> from qiskit.circuit.library import EfficientSU2 >>> prep = PauliFeatureMap(3, reps=3, paulis=['Z', 'YY', 'ZXZ']) >>> wavefunction = EfficientSU2(3) - >>> classifier = prep.compose(wavefunction + >>> classifier = prep.compose(wavefunction) >>> classifier.num_parameters 27 >>> classifier.count_ops() From 581f24784d5267261c06ead8ec9adf303e291b5a Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 17 May 2024 16:49:16 -0400 Subject: [PATCH 076/159] add insert_barrier argument to UnitaryOverlap (#12321) * add insert_barrier argument to UnitaryOverlap * set fold=-1 in circuit drawing --- qiskit/circuit/library/overlap.py | 10 +++++++++- test/python/circuit/library/test_overlap.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index 38f5fb9184e..2db6a80eedc 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -59,7 +59,12 @@ class UnitaryOverlap(QuantumCircuit): """ def __init__( - self, unitary1: QuantumCircuit, unitary2: QuantumCircuit, prefix1="p1", prefix2="p2" + self, + unitary1: QuantumCircuit, + unitary2: QuantumCircuit, + prefix1: str = "p1", + prefix2: str = "p2", + insert_barrier: bool = False, ): """ Args: @@ -69,6 +74,7 @@ def __init__( if it is parameterized. Defaults to ``"p1"``. prefix2: The name of the parameter vector associated to ``unitary2``, if it is parameterized. Defaults to ``"p2"``. + insert_barrier: Whether to insert a barrier between the two unitaries. Raises: CircuitError: Number of qubits in ``unitary1`` and ``unitary2`` does not match. @@ -95,6 +101,8 @@ def __init__( # Generate the actual overlap circuit super().__init__(unitaries[0].num_qubits, name="UnitaryOverlap") self.compose(unitaries[0], inplace=True) + if insert_barrier: + self.barrier() self.compose(unitaries[1].inverse(), inplace=True) diff --git a/test/python/circuit/library/test_overlap.py b/test/python/circuit/library/test_overlap.py index a603f28037b..1a95e3ba915 100644 --- a/test/python/circuit/library/test_overlap.py +++ b/test/python/circuit/library/test_overlap.py @@ -131,6 +131,21 @@ def test_mismatching_qubits(self): with self.assertRaises(CircuitError): _ = UnitaryOverlap(unitary1, unitary2) + def test_insert_barrier(self): + """Test inserting barrier between circuits""" + unitary1 = EfficientSU2(1, reps=1) + unitary2 = EfficientSU2(1, reps=1) + overlap = UnitaryOverlap(unitary1, unitary2, insert_barrier=True) + self.assertEqual(overlap.count_ops()["barrier"], 1) + self.assertEqual( + str(overlap.draw(fold=-1, output="text")).strip(), + """ + ┌───────────────────────────────────────┐ ░ ┌──────────────────────────────────────────┐ +q: ┤ EfficientSU2(p1[0],p1[1],p1[2],p1[3]) ├─░─┤ EfficientSU2_dg(p2[0],p2[1],p2[2],p2[3]) ├ + └───────────────────────────────────────┘ ░ └──────────────────────────────────────────┘ +""".strip(), + ) + if __name__ == "__main__": unittest.main() From 16acb80a03fde1d3e54b75f8ea18ec60661498bf Mon Sep 17 00:00:00 2001 From: Arnau Casau <47946624+arnaucasau@users.noreply.github.com> Date: Mon, 20 May 2024 16:04:42 +0200 Subject: [PATCH 077/159] Remove the duplicated docs for a BackendV1 classmethod (#12443) --- qiskit/providers/backend.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 8ffc7765109..2e551cc311e 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -88,12 +88,6 @@ def __init__(self, configuration, provider=None, **fields): This next bit is necessary just because autosummary generally won't summarise private methods; changing that behaviour would have annoying knock-on effects through all the rest of the documentation, so instead we just hard-code the automethod directive. - - In addition to the public abstract methods, subclasses should also implement the following - private methods: - - .. automethod:: _default_options - :noindex: """ self._configuration = configuration self._options = self._default_options() From 8e3218bc0798b0612edf446db130e95ac9404968 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 20 May 2024 19:10:25 +0100 Subject: [PATCH 078/159] Remove reference to `transpile` from Pulse docs (#12448) * Remove reference to `transpile` from Pulse docs This reference to `transpile` was mistakenly inserted as part of 1a027ac (gh-11565); `execute` used to ignore the `transpile` step for `Schedule`/`ScheduleBlock` and submit directly to `backend.run`. I am not clear if there are actually any backends that *accept* pulse jobs anymore, but if there are, this is what the text should have been translated to. * Update qiskit/pulse/builder.py Co-authored-by: Will Shanks --------- Co-authored-by: Will Shanks --- qiskit/pulse/builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index b7bdbe85c19..8767e8c4e93 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -74,8 +74,8 @@ The builder initializes a :class:`.pulse.Schedule`, ``pulse_prog`` and then begins to construct the program within the context. The output pulse -schedule will survive after the context is exited and can be transpiled and executed like a -normal Qiskit schedule using ``backend.run(transpile(pulse_prog, backend))``. +schedule will survive after the context is exited and can be used like a +normal Qiskit schedule. Pulse programming has a simple imperative style. This leaves the programmer to worry about the raw experimental physics of pulse programming and not From 9d0ae64f6dba20872a5f62a7f2d2cb15983582ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 06:51:40 -0400 Subject: [PATCH 079/159] Bump itertools from 0.12.1 to 0.13.0 (#12427) Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.12.1 to 0.13.0. - [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-itertools/itertools/compare/v0.12.1...v0.13.0) --- updated-dependencies: - dependency-name: itertools dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d812f8fc1c5..27b5cddb313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,9 +605,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1102,7 +1102,7 @@ dependencies = [ "faer-ext", "hashbrown 0.14.5", "indexmap 2.2.6", - "itertools 0.12.1", + "itertools 0.13.0", "ndarray", "num-bigint", "num-complex", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 4f2c80ebff2..0bbe5911a86 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -21,7 +21,7 @@ num-complex = "0.4" num-bigint = "0.4" rustworkx-core = "0.14" faer = "0.18.2" -itertools = "0.12.1" +itertools = "0.13.0" qiskit-circuit.workspace = true [dependencies.smallvec] From fc17d60447fb754ffd87fab04fe5c613d5418968 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 May 2024 11:12:23 -0400 Subject: [PATCH 080/159] Update CITATION.bib file to point to Qiskit whitepaper (#12415) * Update CITATION.bib file to point to Qiskit whitepaper This commit updates the official citation for the Qiskit project to point to the overview paper "Quantum computing with Qiskit" which was recently published on arxiv: https://arxiv.org/abs/2405.08810 * Apply suggestions from code review Co-authored-by: Luciano Bello Co-authored-by: Jake Lishman --------- Co-authored-by: Luciano Bello Co-authored-by: Jake Lishman --- CITATION.bib | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CITATION.bib b/CITATION.bib index a00798d9baa..deac2ef200e 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -1,6 +1,9 @@ -@misc{Qiskit, - author = {{Qiskit contributors}}, - title = {Qiskit: An Open-source Framework for Quantum Computing}, - year = {2023}, - doi = {10.5281/zenodo.2573505} +@misc{qiskit2024, + title={Quantum computing with {Q}iskit}, + author={Javadi-Abhari, Ali and Treinish, Matthew and Krsulich, Kevin and Wood, Christopher J. and Lishman, Jake and Gacon, Julien and Martiel, Simon and Nation, Paul D. and Bishop, Lev S. and Cross, Andrew W. and Johnson, Blake R. and Gambetta, Jay M.}, + year={2024}, + doi={10.48550/arXiv.2405.08810}, + eprint={2405.08810}, + archivePrefix={arXiv}, + primaryClass={quant-ph} } From f08c579ce13a7617a23184f88d28e4a7f98e4412 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 May 2024 12:31:43 -0400 Subject: [PATCH 081/159] Expose internal rust interface to DenseLayout (#12104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose internal rust interface to DenseLayout This commit makes a small change to the rust code for DenseLayout that enables calling it more easily from rust. The primary obstacle was the pyfunction used PyReadonlyArray2 inputs which precludes calling it with rust constructed Array2Views. This adds a new inner public function which takes the array view directly and then the pyfunction's only job is to convert the inputs and outputs to Python. The python side of the function is still building a sparse matrix and then runs reverse Cuthill–McKee to get a permutation of the densest subgraph so any rust consumers will want to keep that in mind (and maybe use sprs to do the same). At the same time it corrects an oversight in the original implementation where the returned numpy arrays of the densest subgraph are copied instead of being returned as references. This should slightly improve performance by eliminating 3 array copies that weren't needed. * Remove PyResult --------- Co-authored-by: Henry Zou <87874865+henryzou50@users.noreply.github.com> --- crates/accelerate/src/dense_layout.rs | 37 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/accelerate/src/dense_layout.rs b/crates/accelerate/src/dense_layout.rs index 7cb54140761..901a906d9c8 100644 --- a/crates/accelerate/src/dense_layout.rs +++ b/crates/accelerate/src/dense_layout.rs @@ -15,8 +15,8 @@ use ahash::RandomState; use hashbrown::{HashMap, HashSet}; use indexmap::IndexSet; use ndarray::prelude::*; +use numpy::IntoPyArray; use numpy::PyReadonlyArray2; -use numpy::ToPyArray; use rayon::prelude::*; use pyo3::prelude::*; @@ -108,10 +108,35 @@ pub fn best_subset( use_error: bool, symmetric_coupling_map: bool, error_matrix: PyReadonlyArray2, -) -> PyResult<(PyObject, PyObject, PyObject)> { +) -> (PyObject, PyObject, PyObject) { let coupling_adj_mat = coupling_adjacency.as_array(); - let coupling_shape = coupling_adj_mat.shape(); let err = error_matrix.as_array(); + let [rows, cols, best_map] = best_subset_inner( + num_qubits, + coupling_adj_mat, + num_meas, + num_cx, + use_error, + symmetric_coupling_map, + err, + ); + ( + rows.into_pyarray_bound(py).into(), + cols.into_pyarray_bound(py).into(), + best_map.into_pyarray_bound(py).into(), + ) +} + +pub fn best_subset_inner( + num_qubits: usize, + coupling_adj_mat: ArrayView2, + num_meas: usize, + num_cx: usize, + use_error: bool, + symmetric_coupling_map: bool, + err: ArrayView2, +) -> [Vec; 3] { + let coupling_shape = coupling_adj_mat.shape(); let avg_meas_err = err.diag().mean().unwrap(); let map_fn = |k| -> SubsetResult { @@ -216,11 +241,7 @@ pub fn best_subset( let rows: Vec = new_cmap.iter().map(|edge| edge[0]).collect(); let cols: Vec = new_cmap.iter().map(|edge| edge[1]).collect(); - Ok(( - rows.to_pyarray_bound(py).into(), - cols.to_pyarray_bound(py).into(), - best_map.to_pyarray_bound(py).into(), - )) + [rows, cols, best_map] } #[pymodule] From 531f91c24bb4f5bcca44bddceb3e2ef336a72044 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 May 2024 15:25:32 -0400 Subject: [PATCH 082/159] Add `DenseLayout` trial to `SabreLayout` (#12453) Building on the work done in #10829, #10721, and #12104 this commit adds a new trial to all runs of `SabreLayout` that runs the dense layout pass. In general the sabre layout algorithm starts from a random layout and then runs a routing algorithm to permute that layout virtually where swaps would be inserted to select a layout that would result in fewer swaps. As was discussed in #10721 and #10829 that random starting point is often not ideal especially for larger targets where the distance between qubits can be quite far. Especially when the circuit qubit count is low relative to the target's qubit count this can result it poor layouts as the distance between the qubits is too large. In qiskit we have an existing pass, `DenseLayout`, which tries to find the most densely connected n qubit subgraph of a connectivity graph. This algorithm necessarily will select a starting layout where the qubits are near each other and for those large backends where the random starting layout doesn't work well this can improve the output quality. As the overhead of `DenseLayout` is quite low and the core algorithm is written in rust already this commit adds a default trial that uses DenseLayout as a starting point on top of the random trials (and any input starting points). For example if the user specifies to run SabreLayout with 20 layout trials this will run 20 random trials and one trial with `DenseLayout` as the starting point. This is all done directly in the sabre layout rust code for efficiency. The other difference between the standalone `DenseLayout` pass is that in the standalone pass a sparse matrix is built and a reverse Cuthill-McKee permutation is run on the densest subgraph qubits to pick the final layout. This permutation is skipped because in the case of Sabre's use of dense layout we're relying on the sabre algorithm to perform the permutation. Depends on: #12104 --- crates/accelerate/src/sabre/layout.rs | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index 1cb539d9598..a1e5e9ce641 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -15,6 +15,7 @@ use pyo3::prelude::*; use pyo3::Python; use hashbrown::HashSet; +use ndarray::prelude::*; use numpy::{IntoPyArray, PyArray, PyReadonlyArray2}; use rand::prelude::*; use rand_pcg::Pcg64Mcg; @@ -29,6 +30,8 @@ use super::sabre_dag::SabreDAG; use super::swap_map::SwapMap; use super::{Heuristic, NodeBlockResults, SabreResult}; +use crate::dense_layout::best_subset_inner; + #[pyfunction] #[pyo3(signature = (dag, neighbor_table, distance_matrix, heuristic, max_iterations, num_swap_trials, num_random_trials, seed=None, partial_layouts=vec![]))] pub fn sabre_layout_and_routing( @@ -52,6 +55,12 @@ pub fn sabre_layout_and_routing( let mut starting_layouts: Vec>> = (0..num_random_trials).map(|_| vec![]).collect(); starting_layouts.append(&mut partial_layouts); + // Run a dense layout trial + starting_layouts.push(compute_dense_starting_layout( + dag.num_qubits, + &target, + run_in_parallel, + )); let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), None => Pcg64Mcg::from_entropy(), @@ -208,3 +217,26 @@ fn layout_trial( .collect(); (initial_layout, final_permutation, sabre_result) } + +fn compute_dense_starting_layout( + num_qubits: usize, + target: &RoutingTargetView, + run_in_parallel: bool, +) -> Vec> { + let mut adj_matrix = target.distance.to_owned(); + if run_in_parallel { + adj_matrix.par_mapv_inplace(|x| if x == 1. { 1. } else { 0. }); + } else { + adj_matrix.mapv_inplace(|x| if x == 1. { 1. } else { 0. }); + } + let [_rows, _cols, map] = best_subset_inner( + num_qubits, + adj_matrix.view(), + 0, + 0, + false, + true, + aview2(&[[0.]]), + ); + map.into_iter().map(|x| Some(x as u32)).collect() +} From 44c0ce3686835f86d30a451babbb7cb34b1f14a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 12:53:57 +0000 Subject: [PATCH 083/159] Bump pulp from 0.18.12 to 0.18.21 (#12457) Bumps [pulp](https://github.com/sarah-ek/pulp) from 0.18.12 to 0.18.21. - [Commits](https://github.com/sarah-ek/pulp/commits) --- updated-dependencies: - dependency-name: pulp 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 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27b5cddb313..e8c855d0b0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "pulp" -version = "0.18.12" +version = "0.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140dfe6dada20716bd5f7284406747c73061a56a0a5d4ad5aee7957c5f71606c" +checksum = "0ec8d02258294f59e4e223b41ad7e81c874aa6b15bc4ced9ba3965826da0eed5" dependencies = [ "bytemuck", "libm", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 0bbe5911a86..de6f41dbfde 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -53,5 +53,5 @@ version = "0.1.0" features = ["ndarray"] [dependencies.pulp] -version = "0.18.12" +version = "0.18.21" features = ["macro"] From 529da36fa6b8d591de2cf2fac837dcd8ed9d16a7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 28 May 2024 09:25:07 -0400 Subject: [PATCH 084/159] Pin nightly rust version to known working build (#12468) In the past couple of days we've seen the miri tests fail in CI while attempting to build crossbeam-epoch from source. We need to do this to ensure that crossbeam-epoch is miri safe so that we can run our own tests of Qiskit's unsafe code in ci. This failure was likely an issue in the recent nightly builds so this commit pins the nightly rust version to one from last week when everything was known to be working. We can remove this specific pin when we know that upstream rust has fixed the issue. --- .github/workflows/miri.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index bdceb20c300..b32a96c3b42 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -14,14 +14,15 @@ jobs: name: Miri runs-on: ubuntu-latest env: - RUSTUP_TOOLCHAIN: nightly + RUSTUP_TOOLCHAIN: nightly-2024-05-24 steps: - uses: actions/checkout@v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@master with: + toolchain: nightly-2024-05-24 components: miri - name: Prepare Miri From 0f0a6347d04620d8ff874458f6e5a722204e9a63 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Tue, 28 May 2024 20:59:38 +0300 Subject: [PATCH 085/159] Fix a bug in isometry.rs (#12469) * remove assertion * extend the test * add release notes * fix release notes * Update releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- crates/accelerate/src/isometry.rs | 1 - releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml | 6 ++++++ test/python/circuit/test_controlled_gate.py | 5 ++--- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index 8d0761666bb..a4e83358a7d 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -212,7 +212,6 @@ fn construct_basis_states( } else if i == target_label { e2 += 1; } else { - assert!(j <= 1); e1 += state_free[j] as usize; e2 += state_free[j] as usize; j += 1 diff --git a/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml b/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml new file mode 100644 index 00000000000..4eeaa9aa3d7 --- /dev/null +++ b/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix a bug in :class:`~.library.Isometry` due to an unnecessary assertion, + that led to an error in :meth:`.UnitaryGate.control` + when :class:`~.library.UnitaryGate` had more that two qubits. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index f0d6dd3a8f7..ced7229415e 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -852,10 +852,9 @@ def test_controlled_unitary(self, num_ctrl_qubits): self.assertTrue(is_unitary_matrix(base_mat)) self.assertTrue(matrix_equal(cop_mat, test_op.data)) - @data(1, 2, 3, 4, 5) - def test_controlled_random_unitary(self, num_ctrl_qubits): + @combine(num_ctrl_qubits=(1, 2, 3, 4, 5), num_target=(2, 3)) + def test_controlled_random_unitary(self, num_ctrl_qubits, num_target): """Test the matrix data of an Operator based on a random UnitaryGate.""" - num_target = 2 base_gate = random_unitary(2**num_target).to_instruction() base_mat = base_gate.to_matrix() cgate = base_gate.control(num_ctrl_qubits) From 473c3c2e58f855332532c2c47a11db5695c0180f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 18:12:33 +0000 Subject: [PATCH 086/159] Bump faer from 0.18.2 to 0.19.0 (#12466) * Bump faer from 0.18.2 to 0.19.0 Bumps [faer](https://github.com/sarah-ek/faer-rs) from 0.18.2 to 0.19.0. - [Changelog](https://github.com/sarah-ek/faer-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sarah-ek/faer-rs/commits) --- updated-dependencies: - dependency-name: faer dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump faer-ext to 0.2.0 too The faer and faer-ext versions need to be kept in sync. This commit bumps the faer-ext version as part of the dependabot automated bump for faer to do this. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthew Treinish --- Cargo.lock | 120 ++++++++++++++++++++++++++++------- crates/accelerate/Cargo.toml | 4 +- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8c855d0b0e..cf8e4f365df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,18 +281,18 @@ dependencies = [ [[package]] name = "equator" -version = "0.1.10" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b0a88aa91d0ad2b9684e4479aed31a17d3f9051bdbbc634bd2c01bc5a5eee8" +checksum = "c35da53b5a021d2484a7cc49b2ac7f2d840f8236a286f84202369bd338d761ea" dependencies = [ "equator-macro", ] [[package]] name = "equator-macro" -version = "0.1.9" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae" +checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" dependencies = [ "proc-macro2", "quote", @@ -307,9 +307,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e547492d9b55c4ea882584e691ed092228981e337d0c800bc721301d7e61e40a" +checksum = "91ef9e1a4098a9e3a03c47bc5061406c04820552d869fd0fcd92587d07b271f0" dependencies = [ "bytemuck", "coe-rs", @@ -321,6 +321,7 @@ dependencies = [ "libm", "matrixcompare", "matrixcompare-core", + "nano-gemm", "npyz", "num-complex", "num-traits", @@ -334,9 +335,9 @@ dependencies = [ [[package]] name = "faer-entity" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ea5c06233193392c614a46aa3bbe3de29c1404692c8053abd9c2765a1cd159" +checksum = "ab968a02be27be95de0f1ad0af901b865fa0866b6a9b553a6cc9cf7f19c2ce71" dependencies = [ "bytemuck", "coe-rs", @@ -349,9 +350,9 @@ dependencies = [ [[package]] name = "faer-ext" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f67e0c5be50b08c57b59f1cf78a1c8399f6816f4e1a2e0801470ff58dad23a3" +checksum = "4cf30f6ae73f372c0e0cf7556c44e50f1eee0a714d71396091613d68c43625c9" dependencies = [ "faer", "ndarray", @@ -366,9 +367,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "gemm" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" +checksum = "e400f2ffd14e7548356236c35dc39cad6666d833a852cb8a8f3f28029359bb03" dependencies = [ "dyn-stack", "gemm-c32", @@ -386,9 +387,9 @@ dependencies = [ [[package]] name = "gemm-c32" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" +checksum = "10dc4a6176c8452d60eac1a155b454c91c668f794151a303bf3c75ea2874812d" dependencies = [ "dyn-stack", "gemm-common", @@ -401,9 +402,9 @@ dependencies = [ [[package]] name = "gemm-c64" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" +checksum = "cc2032ce2c0bb150da0256338759a6fb01ca056f6dfe28c4d14af32d7f878f6f" dependencies = [ "dyn-stack", "gemm-common", @@ -416,9 +417,9 @@ dependencies = [ [[package]] name = "gemm-common" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" +checksum = "90fd234fc525939654f47b39325fd5f55e552ceceea9135f3aa8bdba61eabef6" dependencies = [ "bytemuck", "dyn-stack", @@ -436,9 +437,9 @@ dependencies = [ [[package]] name = "gemm-f16" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" +checksum = "3fc3652651f96a711d46b8833e1fac27a864be4bdfa81a374055f33ddd25c0c6" dependencies = [ "dyn-stack", "gemm-common", @@ -454,9 +455,9 @@ dependencies = [ [[package]] name = "gemm-f32" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" +checksum = "acbc51c44ae3defd207e6d9416afccb3c4af1e7cef5e4960e4c720ac4d6f998e" dependencies = [ "dyn-stack", "gemm-common", @@ -469,9 +470,9 @@ dependencies = [ [[package]] name = "gemm-f64" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" +checksum = "3f37fc86e325c2415a4d0cab8324a0c5371ec06fc7d2f9cb1636fcfc9536a8d8" dependencies = [ "dyn-stack", "gemm-common", @@ -696,6 +697,76 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "nano-gemm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f563548d38f390ef9893e4883ec38c1fb312f569e98d76bededdd91a3b41a043" +dependencies = [ + "equator", + "nano-gemm-c32", + "nano-gemm-c64", + "nano-gemm-codegen", + "nano-gemm-core", + "nano-gemm-f32", + "nano-gemm-f64", + "num-complex", +] + +[[package]] +name = "nano-gemm-c32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40449e57a5713464c3a1208c4c3301c8d29ee1344711822cf022bc91373a91b" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-c64" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743a6e6211358fba85d1009616751e4107da86f4c95b24e684ce85f25c25b3bf" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-codegen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963bf7c7110d55430169dc74c67096375491ed580cd2ef84842550ac72e781fa" + +[[package]] +name = "nano-gemm-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3fc4f83ae8861bad79dc3c016bd6b0220da5f9de302e07d3112d16efc24aa6" + +[[package]] +name = "nano-gemm-f32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3681b7ce35658f79da94b7f62c60a005e29c373c7111ed070e3bf64546a8bb" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + +[[package]] +name = "nano-gemm-f64" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1e619ed04d801809e1f63e61b669d380c4119e8b0cdd6ed184c6b111f046d8" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + [[package]] name = "ndarray" version = "0.15.6" @@ -740,6 +811,7 @@ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", + "rand", ] [[package]] diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index de6f41dbfde..63be9ad90b4 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" num-complex = "0.4" num-bigint = "0.4" rustworkx-core = "0.14" -faer = "0.18.2" +faer = "0.19.0" itertools = "0.13.0" qiskit-circuit.workspace = true @@ -49,7 +49,7 @@ workspace = true features = ["rayon"] [dependencies.faer-ext] -version = "0.1.0" +version = "0.2.0" features = ["ndarray"] [dependencies.pulp] From df379876ba10d6f490a96723b6dbbf723ec45d7a Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Wed, 29 May 2024 13:27:54 -0400 Subject: [PATCH 087/159] Removing _name attribute from Parameter (#12191) * removing _name attribute from Parameter * lint fix for parameter update * fix the __getstate__ structure --- qiskit/circuit/parameter.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 4d0f73cf077..abe4e61adf6 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -59,7 +59,7 @@ class Parameter(ParameterExpression): bc.draw('mpl') """ - __slots__ = ("_name", "_uuid", "_hash") + __slots__ = ("_uuid", "_hash") # This `__init__` does not call the super init, because we can't construct the # `_parameter_symbols` dictionary we need to pass to it before we're entirely initialised @@ -79,7 +79,6 @@ def __init__( field when creating two parameters to the same thing (along with the same name) allows them to be equal. This is useful during serialization and deserialization. """ - self._name = name self._uuid = uuid4() if uuid is None else uuid symbol = symengine.Symbol(name) @@ -117,7 +116,7 @@ def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False): @property def name(self): """Returns the name of the :class:`Parameter`.""" - return self._name + return self._symbol_expr.name @property def uuid(self) -> UUID: @@ -143,7 +142,7 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, Parameter): - return (self._uuid, self._name) == (other._uuid, other._name) + return (self._uuid, self._symbol_expr) == (other._uuid, other._symbol_expr) elif isinstance(other, ParameterExpression): return super().__eq__(other) else: @@ -155,7 +154,7 @@ def _hash_key(self): # expression, so its full hash key is split into `(parameter_keys, symbolic_expression)`. # This method lets containing expressions get only the bits they need for equality checks in # the first value, without wasting time re-hashing individual Sympy/Symengine symbols. - return (self._name, self._uuid) + return (self._symbol_expr, self._uuid) def __hash__(self): # This is precached for performance, since it's used a lot and we are immutable. @@ -165,10 +164,10 @@ def __hash__(self): # operation attempts to put this parameter into a hashmap. def __getstate__(self): - return (self._name, self._uuid, self._symbol_expr) + return (self.name, self._uuid, self._symbol_expr) def __setstate__(self, state): - self._name, self._uuid, self._symbol_expr = state + _, self._uuid, self._symbol_expr = state self._parameter_keys = frozenset((self._hash_key(),)) self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: self._symbol_expr} From 61323662a7b7349aa1d47fc0d39f3bf04fdbc931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 31 May 2024 14:43:10 +0200 Subject: [PATCH 088/159] Update `transpile()` and `generate_preset_pass_manager` to convert loose input of constraints to a `Target` with `Target.from_configuration()` (#12185) * Update transpile() to convert BackendV1 inputs to BackendV2 with BackendV2Converter * Rework use of instruction durations, move logic from transpile function to individual passes. * Convert loose constraints to target inside transpile. Cover edge cases where num_qubits and/or basis_gates is None. * Fix basis gates oversights, characterize errors in test_transpiler.py and comment out failing cases (temporary). * Fix pulse oversights * Add backend instructions to name mapping by default * Fix edge case 3: Add control flow to default basis gates * Add path for regular cases and path for edge cases that skips target construction. * Complete edge case handling, fix tests. * Update reno * Apply suggestion from Ray's code review Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * * Migrate full target-building capabilities from transpile to generate_preset_pass_manager. * Apply Matt's suggestions for custom mapping and control flow op names. * Extend docstring, fix typos. * Fix lint, update reno * Create new reno for 1.2 instead of modifying the 1.1 one. --------- Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- qiskit/compiler/transpiler.py | 119 ++------ .../preset_passmanagers/__init__.py | 280 +++++++++++++++++- qiskit/transpiler/target.py | 7 +- ...n-generate-preset-pm-5215e00d22d0205c.yaml | 14 + test/python/compiler/test_transpiler.py | 8 +- test/python/transpiler/test_sabre_swap.py | 2 +- .../python/transpiler/test_stochastic_swap.py | 2 +- 7 files changed, 314 insertions(+), 118 deletions(-) create mode 100644 releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 95c583ceeaa..9dd316839a0 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -13,7 +13,6 @@ # pylint: disable=invalid-sequence-index """Circuit transpile function""" -import copy import logging from time import time from typing import List, Union, Dict, Callable, Any, Optional, TypeVar @@ -30,11 +29,10 @@ from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget -from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType +from qiskit.transpiler.instruction_durations import InstructionDurationsType from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.transpiler.timing_constraints import TimingConstraints -from qiskit.transpiler.target import Target, target_to_backend_properties +from qiskit.transpiler.target import Target logger = logging.getLogger(__name__) @@ -335,73 +333,32 @@ def callback_func(**kwargs): UserWarning, ) - _skip_target = False - _given_inst_map = bool(inst_map) # check before inst_map is overwritten - # If a target is specified, have it override any implicit selections from a backend - if target is not None: - if coupling_map is None: - coupling_map = target.build_coupling_map() - if basis_gates is None: - basis_gates = list(target.operation_names) - if instruction_durations is None: - instruction_durations = target.durations() - if inst_map is None: - inst_map = target.instruction_schedule_map() - if dt is None: - dt = target.dt - if timing_constraints is None: - timing_constraints = target.timing_constraints() - if backend_properties is None: - backend_properties = target_to_backend_properties(target) - # If target is not specified and any hardware constraint object is - # manually specified, do not use the target from the backend as - # it is invalidated by a custom basis gate list, custom coupling map, - # custom dt or custom instruction_durations - elif ( - basis_gates is not None # pylint: disable=too-many-boolean-expressions - or coupling_map is not None - or dt is not None - or instruction_durations is not None - or backend_properties is not None - or timing_constraints is not None - ): - _skip_target = True - else: - target = getattr(backend, "target", None) + if not ignore_backend_supplied_default_methods: + if scheduling_method is None and hasattr(backend, "get_scheduling_stage_plugin"): + scheduling_method = backend.get_scheduling_stage_plugin() + if translation_method is None and hasattr(backend, "get_translation_stage_plugin"): + translation_method = backend.get_translation_stage_plugin() initial_layout = _parse_initial_layout(initial_layout) - coupling_map = _parse_coupling_map(coupling_map, backend) approximation_degree = _parse_approximation_degree(approximation_degree) - output_name = _parse_output_name(output_name, circuits) - inst_map = _parse_inst_map(inst_map, backend) + coupling_map = _parse_coupling_map(coupling_map) _check_circuits_coupling_map(circuits, coupling_map, backend) - timing_constraints = _parse_timing_constraints(backend, timing_constraints) - instruction_durations = _parse_instruction_durations(backend, instruction_durations, dt) - - if _given_inst_map and inst_map.has_custom_gate() and target is not None: - # Do not mutate backend target - target = copy.deepcopy(target) - target.update_from_instruction_schedule_map(inst_map) - - if not ignore_backend_supplied_default_methods: - if scheduling_method is None and hasattr(backend, "get_scheduling_stage_plugin"): - scheduling_method = backend.get_scheduling_stage_plugin() - if translation_method is None and hasattr(backend, "get_translation_stage_plugin"): - translation_method = backend.get_translation_stage_plugin() - + # Edge cases require using the old model (loose constraints) instead of building a target, + # but we don't populate the passmanager config with loose constraints unless it's one of + # the known edge cases to control the execution path. pm = generate_preset_pass_manager( optimization_level, - backend=backend, target=target, + backend=backend, basis_gates=basis_gates, - inst_map=inst_map, coupling_map=coupling_map, instruction_durations=instruction_durations, backend_properties=backend_properties, timing_constraints=timing_constraints, + inst_map=inst_map, initial_layout=initial_layout, layout_method=layout_method, routing_method=routing_method, @@ -414,14 +371,15 @@ def callback_func(**kwargs): hls_config=hls_config, init_method=init_method, optimization_method=optimization_method, - _skip_target=_skip_target, + dt=dt, ) + out_circuits = pm.run(circuits, callback=callback, num_processes=num_processes) + for name, circ in zip(output_name, out_circuits): circ.name = name end_time = time() _log_transpile_time(start_time, end_time) - if arg_circuits_list: return out_circuits else: @@ -451,31 +409,20 @@ def _log_transpile_time(start_time, end_time): logger.info(log_msg) -def _parse_inst_map(inst_map, backend): - # try getting inst_map from user, else backend - if inst_map is None and backend is not None: - inst_map = backend.target.instruction_schedule_map() - return inst_map - - -def _parse_coupling_map(coupling_map, backend): - # try getting coupling_map from user, else backend - if coupling_map is None and backend is not None: - coupling_map = backend.coupling_map - +def _parse_coupling_map(coupling_map): # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] - if coupling_map is None or isinstance(coupling_map, CouplingMap): - return coupling_map if isinstance(coupling_map, list) and all( isinstance(i, list) and len(i) == 2 for i in coupling_map ): return CouplingMap(coupling_map) - else: + elif isinstance(coupling_map, list): raise TranspilerError( "Only a single input coupling map can be used with transpile() if you need to " "target different coupling maps for different circuits you must call transpile() " "multiple times" ) + else: + return coupling_map def _parse_initial_layout(initial_layout): @@ -491,22 +438,6 @@ def _parse_initial_layout(initial_layout): return initial_layout -def _parse_instruction_durations(backend, inst_durations, dt): - """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided, - the backend will be ignored, otherwise, the durations will be populated from the - backend. - """ - final_durations = InstructionDurations() - if not inst_durations: - backend_durations = InstructionDurations() - if backend is not None: - backend_durations = backend.instruction_durations - final_durations.update(backend_durations, dt or backend_durations.dt) - else: - final_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) - return final_durations - - def _parse_approximation_degree(approximation_degree): if approximation_degree is None: return None @@ -549,13 +480,3 @@ def _parse_output_name(output_name, circuits): ) else: return [circuit.name for circuit in circuits] - - -def _parse_timing_constraints(backend, timing_constraints): - if isinstance(timing_constraints, TimingConstraints): - return timing_constraints - if backend is None and timing_constraints is None: - timing_constraints = TimingConstraints() - elif backend is not None: - timing_constraints = backend.target.timing_constraints() - return timing_constraints diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 8d653ed3a1a..37b284a4680 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 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 @@ -21,7 +21,8 @@ for the transpiler. The preset pass managers are instances of :class:`~.StagedPassManager` which are used to execute the circuit transformations as part of Qiskit's compiler inside the -:func:`~.transpile` function at the different optimization levels. +:func:`~.transpile` function at the different optimization levels, but +can also be used in a standalone manner. The functionality here is divided into two parts, the first includes the functions used generate the entire pass manager which is used by :func:`~.transpile` (:ref:`preset_pass_manager_generators`) and the @@ -56,13 +57,21 @@ .. autofunction:: generate_scheduling .. currentmodule:: qiskit.transpiler.preset_passmanagers """ +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 +from qiskit.providers.backend_compat import BackendV2Converter + +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.target import target_to_backend_properties +from qiskit.transpiler.target import Target, target_to_backend_properties from qiskit.transpiler import CouplingMap +from qiskit.transpiler.exceptions import TranspilerError + from .level0 import level_0_pass_manager from .level1 import level_1_pass_manager from .level2 import level_2_pass_manager @@ -91,16 +100,43 @@ def generate_preset_pass_manager( hls_config=None, init_method=None, optimization_method=None, + dt=None, *, _skip_target=False, ): """Generate a preset :class:`~.PassManager` - This function is used to quickly generate a preset pass manager. A preset pass - manager are the default pass managers used by the :func:`~.transpile` + This function is used to quickly generate a preset pass manager. Preset pass + managers are the default pass managers used by the :func:`~.transpile` function. This function provides a convenient and simple method to construct a standalone :class:`~.PassManager` object that mirrors what the transpile + function internally builds and uses. + + The target constraints for the pass manager construction can be specified through a :class:`.Target` + instance, a `.BackendV1` or `.BackendV2` instance, or via loose constraints (``basis_gates``, + ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + ``dt`` or ``timing_constraints``). + The order of priorities for target constraints works as follows: if a ``target`` + input is provided, it will take priority over any ``backend`` input or loose constraints. + If a ``backend`` is provided together with any loose constraint + from the list above, the loose constraint will take priority over the corresponding backend + constraint. This behavior is independent of whether the ``backend`` instance is of type + :class:`.BackendV1` or :class:`.BackendV2`, as summarized in the table below. The first column + in the table summarizes the potential user-provided constraints, and each cell shows whether + the priority is assigned to that specific constraint input or another input + (`target`/`backend(V1)`/`backend(V2)`). + ============================ ========= ======================== ======================= + User Provided target backend(V1) backend(V2) + ============================ ========= ======================== ======================= + **basis_gates** target basis_gates basis_gates + **coupling_map** target coupling_map coupling_map + **instruction_durations** target instruction_durations instruction_durations + **inst_map** target inst_map inst_map + **dt** target dt dt + **timing_constraints** target timing_constraints timing_constraints + **backend_properties** target backend_properties backend_properties + ============================ ========= ======================== ======================= Args: optimization_level (int): The optimization level to generate a @@ -126,16 +162,57 @@ def generate_preset_pass_manager( and ``backend_properties``. basis_gates (list): List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). - inst_map (InstructionScheduleMap): Mapping object that maps gate to schedules. + inst_map (InstructionScheduleMap): Mapping object that maps gates to schedules. If any user defined calibration is found in the map and this is used in a circuit, transpiler attaches the custom gate definition to the circuit. This enables one to flexibly override the low-level instruction implementation. coupling_map (CouplingMap or list): Directed graph represented a coupling - map. - instruction_durations (InstructionDurations): Dictionary of duration - (in dt) for each instruction. + map. Multiple formats are supported: + + #. ``CouplingMap`` instance + #. List, must be given as an adjacency matrix, where each entry + specifies all directed two-qubit interactions supported by backend, + e.g: ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]`` + + instruction_durations (InstructionDurations or list): Dictionary of duration + (in dt) for each instruction. If specified, these durations overwrite the + gate lengths in ``backend.properties``. Applicable only if ``scheduling_method`` + is specified. + The format of ``instruction_durations`` must be as follows: + They must be given as an :class:`.InstructionDurations` instance or a list of tuples + + ``` + [(instruction_name, qubits, duration, unit), ...]. + | [('cx', [0, 1], 12.3, 'ns'), ('u3', [0], 4.56, 'ns')] + | [('cx', [0, 1], 1000), ('u3', [0], 300)] + ``` + + If ``unit`` is omitted, the default is ``'dt'``, which is a sample time depending on backend. + If the time unit is ``'dt'``, the duration must be an integer. + dt (float): Backend sample time (resolution) in seconds. + If provided, this value will overwrite the ``dt`` value in ``instruction_durations``. + If ``None`` (default) and a backend is provided, ``backend.dt`` is used. timing_constraints (TimingConstraints): Hardware time alignment restrictions. + A quantum computer backend may report a set of restrictions, namely: + + - granularity: An integer value representing minimum pulse gate + resolution in units of ``dt``. A user-defined pulse gate should have + duration of a multiple of this granularity value. + - min_length: An integer value representing minimum pulse gate + length in units of ``dt``. A user-defined pulse gate should be longer + than this length. + - pulse_alignment: An integer value representing a time resolution of gate + instruction starting time. Gate instruction should start at time which + is a multiple of the alignment value. + - acquire_alignment: An integer value representing a time resolution of measure + instruction starting time. Measure instruction should start at time which + is a multiple of the alignment value. + + This information will be provided by the backend configuration. + If the backend doesn't have any restriction on the instruction time allocation, + then ``timing_constraints`` is None and no adjustment will be performed. + initial_layout (Layout | List[int]): Initial position of virtual qubits on physical qubits. layout_method (str): The :class:`~.Pass` to use for choosing initial qubit @@ -205,8 +282,74 @@ def generate_preset_pass_manager( ValueError: if an invalid value for ``optimization_level`` is passed in. """ - if coupling_map is not None and not isinstance(coupling_map, CouplingMap): - coupling_map = CouplingMap(coupling_map) + 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) + + # Check if a custom inst_map was specified before overwriting inst_map + _given_inst_map = bool(inst_map) + # If there are no loose constraints => use backend target if available + _no_loose_constraints = ( + basis_gates is None + and coupling_map is None + and dt is None + and instruction_durations is None + and backend_properties is None + and timing_constraints is None + ) + # If it's an edge case => do not build target + _skip_target = ( + target is None + and backend is None + and (basis_gates is None or coupling_map is None or instruction_durations is not None) + ) + + # Resolve loose constraints case-by-case against backend constraints. + # The order of priority is loose constraints > backend. + dt = _parse_dt(dt, backend) + instruction_durations = _parse_instruction_durations(backend, instruction_durations, dt) + timing_constraints = _parse_timing_constraints(backend, timing_constraints) + inst_map = _parse_inst_map(inst_map, backend) + # The basis gates parser will set _skip_target to True if a custom basis gate is found + # (known edge case). + basis_gates, name_mapping, _skip_target = _parse_basis_gates( + basis_gates, backend, inst_map, _skip_target + ) + coupling_map = _parse_coupling_map(coupling_map, backend) + + if target is None: + if backend is not None and _no_loose_constraints: + # If a backend is specified without loose constraints, use its target directly. + target = backend.target + elif not _skip_target: + # Only parse backend properties when the target isn't skipped to + # preserve the former behavior of transpile. + backend_properties = _parse_backend_properties(backend_properties, backend) + # Build target from constraints. + target = Target.from_configuration( + basis_gates=basis_gates, + num_qubits=backend.num_qubits if backend is not None else None, + coupling_map=coupling_map, + # If the instruction map has custom gates, do not give as config, the information + # will be added to the target with update_from_instruction_schedule_map + inst_map=inst_map if inst_map and not inst_map.has_custom_gate() else None, + backend_properties=backend_properties, + instruction_durations=instruction_durations, + concurrent_measurements=( + backend.target.concurrent_measurements if backend is not None else None + ), + dt=dt, + timing_constraints=timing_constraints, + custom_name_mapping=name_mapping, + ) + + # Update target with custom gate information. Note that this is an exception to the priority + # order (target > loose constraints), added to handle custom gates for scheduling passes. + if target is not None and _given_inst_map and inst_map.has_custom_gate(): + target = copy.deepcopy(target) + target.update_from_instruction_schedule_map(inst_map) if target is not None: if coupling_map is None: @@ -262,6 +405,119 @@ def generate_preset_pass_manager( return pm +def _parse_basis_gates(basis_gates, backend, inst_map, skip_target): + name_mapping = {} + standard_gates = get_standard_gate_name_mapping() + # Add control flow gates by default to basis set + default_gates = {"measure", "delay", "reset"}.union(CONTROL_FLOW_OP_NAMES) + + try: + instructions = set(basis_gates) + for name in default_gates: + if name not in instructions: + instructions.add(name) + except TypeError: + instructions = None + + if backend is None: + # Check for custom instructions + if instructions is None: + return None, name_mapping, skip_target + + for inst in instructions: + if inst not in standard_gates or inst not in default_gates: + skip_target = True + break + + return list(instructions), name_mapping, skip_target + + instructions = instructions or backend.operation_names + name_mapping.update( + {name: backend.target.operation_from_name(name) for name in backend.operation_names} + ) + + # Check for custom instructions before removing calibrations + for inst in instructions: + if inst not in standard_gates or inst not in default_gates: + skip_target = True + break + + # Remove calibrated instructions, as they will be added later from the instruction schedule map + if inst_map is not None and not skip_target: + for inst in inst_map.instructions: + for qubit in inst_map.qubits_with_instruction(inst): + entry = inst_map._get_calibration_entry(inst, qubit) + if entry.user_provided and inst in instructions: + instructions.remove(inst) + + return list(instructions) if instructions else None, name_mapping, skip_target + + +def _parse_inst_map(inst_map, backend): + # try getting inst_map from user, else backend + if inst_map is None and backend is not None: + inst_map = backend.target.instruction_schedule_map() + return inst_map + + +def _parse_backend_properties(backend_properties, backend): + # try getting backend_props from user, else backend + if backend_properties is None and backend is not None: + backend_properties = target_to_backend_properties(backend.target) + return backend_properties + + +def _parse_dt(dt, backend): + # try getting dt from user, else backend + if dt is None and backend is not None: + dt = backend.target.dt + return dt + + +def _parse_coupling_map(coupling_map, backend): + # try getting coupling_map from user, else backend + if coupling_map is None and backend is not None: + coupling_map = backend.coupling_map + + # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] + if coupling_map is None or isinstance(coupling_map, CouplingMap): + return coupling_map + if isinstance(coupling_map, list) and all( + isinstance(i, list) and len(i) == 2 for i in coupling_map + ): + return CouplingMap(coupling_map) + else: + raise TranspilerError( + "Only a single input coupling map can be used with generate_preset_pass_manager()." + ) + + +def _parse_instruction_durations(backend, inst_durations, dt): + """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided, + the backend will be ignored, otherwise, the durations will be populated from the + backend. + """ + final_durations = InstructionDurations() + if not inst_durations: + backend_durations = InstructionDurations() + if backend is not None: + backend_durations = backend.instruction_durations + final_durations.update(backend_durations, dt or backend_durations.dt) + else: + final_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) + return final_durations + + +def _parse_timing_constraints(backend, timing_constraints): + if isinstance(timing_constraints, TimingConstraints): + return timing_constraints + if backend is None and timing_constraints is None: + timing_constraints = TimingConstraints() + elif backend is not None: + timing_constraints = backend.target.timing_constraints() + return timing_constraints + + __all__ = [ "level_0_pass_manager", "level_1_pass_manager", diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 5af9e868658..f7b5227c026 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -426,7 +426,9 @@ def add_instruction(self, instruction, properties=None, name=None): f"of qubits in the properties dictionary: {qarg}" ) if qarg is not None: - self.num_qubits = max(self.num_qubits, max(qarg) + 1) + self.num_qubits = max( + self.num_qubits if self.num_qubits is not None else 0, max(qarg) + 1 + ) qargs_val[qarg] = properties[qarg] self._qarg_gate_map[qarg].add(instruction_name) self._gate_map[instruction_name] = qargs_val @@ -987,7 +989,8 @@ def instruction_properties(self, index): def _build_coupling_graph(self): self._coupling_graph = rx.PyDiGraph(multigraph=False) - self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) + if self.num_qubits is not None: + self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) for gate, qarg_map in self._gate_map.items(): if qarg_map is None: if self._gate_name_map[gate].num_qubits == 2: diff --git a/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml b/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml new file mode 100644 index 00000000000..4857bb1bda1 --- /dev/null +++ b/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml @@ -0,0 +1,14 @@ +--- +features_transpiler: + - | + A new ``dt`` argument has been added to :func:`.generate_preset_pass_manager` to match + the set of arguments of :func:`.transpile`. This will allow for the internal conversion + of transpilation constraints to a :class:`.Target` representation. + +upgrade_transpiler: + - | + The :func:`.generate_preset_pass_manager` function has been upgraded to, when possible, + internally convert transpiler constraints into a :class:`.Target` instance. + If a `backend` input of type :class:`.BackendV1` is provided, it will be + converted to :class:`.BackendV2` to expose its :class:`.Target`. This change does + not require any user action. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 6166528f886..30ba83440c9 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1829,7 +1829,7 @@ def test_synthesis_translation_method_with_single_qubit_gates(self, optimization @data(0, 1, 2, 3) def test_synthesis_translation_method_with_gates_outside_basis(self, optimization_level): - """Test that synthesis translation works for circuits with single gates outside bassis""" + """Test that synthesis translation works for circuits with single gates outside basis""" qc = QuantumCircuit(2) qc.swap(0, 1) res = transpile( @@ -2781,12 +2781,14 @@ def test_backend_and_custom_gate(self, opt_level): backend = GenericBackendV2( num_qubits=5, coupling_map=[[0, 1], [1, 0], [1, 2], [1, 3], [2, 1], [3, 1], [3, 4], [4, 3]], + seed=42, ) inst_map = InstructionScheduleMap() inst_map.add("newgate", [0, 1], pulse.ScheduleBlock()) newgate = Gate("newgate", 2, []) circ = QuantumCircuit(2) circ.append(newgate, [0, 1]) + tqc = transpile( circ, backend, @@ -2797,8 +2799,8 @@ def test_backend_and_custom_gate(self, opt_level): ) self.assertEqual(len(tqc.data), 1) self.assertEqual(tqc.data[0].operation, newgate) - qubits = tuple(tqc.find_bit(x).index for x in tqc.data[0].qubits) - self.assertIn(qubits, backend.target.qargs) + for x in tqc.data[0].qubits: + self.assertIn((tqc.find_bit(x).index,), backend.target.qargs) @ddt diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 5315c4b8e01..fbe4e1fbf74 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -1329,9 +1329,9 @@ 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.basis_gates.update(["for_loop", "while_loop", "if_else"]) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 5b924e590a5..8c96150ae8f 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -1489,9 +1489,9 @@ class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase): 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.basis_gates.update(["for_loop", "while_loop", "if_else"]) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" From 9d03b4bf610e41c17227ecefda7feba5262d348e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quinten=20Prei=C3=9F?= <90014830+J-C-Q@users.noreply.github.com> Date: Sat, 1 Jun 2024 04:47:39 +0200 Subject: [PATCH 089/159] Update operation.py docs (#12485) One "add" too much :) --- qiskit/circuit/operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/operation.py b/qiskit/circuit/operation.py index c299e130178..8856222b266 100644 --- a/qiskit/circuit/operation.py +++ b/qiskit/circuit/operation.py @@ -25,7 +25,7 @@ class Operation(ABC): :class:`~qiskit.circuit.Reset`, :class:`~qiskit.circuit.Barrier`, :class:`~qiskit.circuit.Measure`, and operators such as :class:`~qiskit.quantum_info.Clifford`. - The main purpose is to add allow abstract mathematical objects to be added directly onto + The main purpose is to allow abstract mathematical objects to be added directly onto abstract circuits, and for the exact syntheses of these to be determined later, during compilation. From 797bb285632a4b58c35c9e4cf5be44502b5bd78a Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 4 Jun 2024 01:39:55 +0200 Subject: [PATCH 090/159] Add __pos__ for Parameter (#12496) * Add __pos__ for ParameterExpression * docstring * Update qiskit/circuit/parameterexpression.py Co-authored-by: Jake Lishman * Add release note * replace 1.0 by 1 * black * Reword release note --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- qiskit/circuit/parameterexpression.py | 5 ++++- ...unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml | 4 ++++ test/python/circuit/test_parameters.py | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 4f6453f90f4..2b81ddd769f 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -327,8 +327,11 @@ def __rsub__(self, other): def __mul__(self, other): return self._apply_operation(operator.mul, other) + def __pos__(self): + return self._apply_operation(operator.mul, 1) + def __neg__(self): - return self._apply_operation(operator.mul, -1.0) + return self._apply_operation(operator.mul, -1) def __rmul__(self, other): return self._apply_operation(operator.mul, other, reflected=True) diff --git a/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml b/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml new file mode 100644 index 00000000000..92fc63d4989 --- /dev/null +++ b/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml @@ -0,0 +1,4 @@ +--- +features_circuits: + - | + :class:`.ParameterExpression` now supports the unary ``+`` operator. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 7bcc2cd35f3..ed82f33eac9 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1755,6 +1755,13 @@ def test_negated_expression(self): self.assertEqual(float(bound_expr2), 3) + def test_positive_expression(self): + """This tests parameter unary plus.""" + x = Parameter("x") + y = +x + self.assertEqual(float(y.bind({x: 1})), 1.0) + self.assertIsInstance(+x, type(-x)) + def test_standard_cu3(self): """This tests parameter negation in standard extension gate cu3.""" from qiskit.circuit.library import CU3Gate From 72ff2cf20e1047236c15f27f08c384c40f4a82f8 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Tue, 4 Jun 2024 14:12:06 -0400 Subject: [PATCH 091/159] Implement `num_qubits` and `num_clbits` in Rust (#12495) * Implement num_qubits and num_clbits in Rust * QuantumCircuit.num_qubits and num_clbits are twice as fast after this PR. * These are used in several places. For example QuantumCircuit.width() is three times faster. * num_qubits and num_clbits are introduced in circuit_data.rs. These functions are called by the corresponding Python methods. They are also used in circuit_data.rs itself. * Add doc strings for rust implemented num_qubits and num_clbits * Add doc string for `width` in Rust --- crates/circuit/src/circuit_data.rs | 49 ++++++++++++++++++++++++------ qiskit/circuit/quantumcircuit.py | 28 ++++++++--------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 944565cf36d..d8eca9ef854 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -220,6 +220,16 @@ impl CircuitData { self.qubits.clone_ref(py) } + /// Return the number of qubits. This is equivalent to the length of the list returned by + /// :meth:`.CircuitData.qubits` + /// + /// Returns: + /// int: The number of qubits. + #[getter] + pub fn num_qubits(&self) -> usize { + self.qubits_native.len() + } + /// Returns the current sequence of registered :class:`.Clbit` /// instances as a list. /// @@ -235,6 +245,25 @@ impl CircuitData { self.clbits.clone_ref(py) } + /// Return the number of clbits. This is equivalent to the length of the list returned by + /// :meth:`.CircuitData.clbits`. + /// + /// Returns: + /// int: The number of clbits. + #[getter] + pub fn num_clbits(&self) -> usize { + self.clbits_native.len() + } + + /// Return the width of the circuit. This is the number of qubits plus the + /// number of clbits. + /// + /// Returns: + /// int: The width of the circuit. + pub fn width(&self) -> usize { + self.num_qubits() + self.num_clbits() + } + /// Registers a :class:`.Qubit` instance. /// /// Args: @@ -246,13 +275,13 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.qubits_native.len() != self.qubits.bind(bit.py()).len() { + if self.num_qubits() != self.qubits.bind(bit.py()).len() { return Err(PyRuntimeError::new_err(concat!( "This circuit's 'qubits' list has become out of sync with the circuit data.", " Did something modify it?" ))); } - let idx: BitType = self.qubits_native.len().try_into().map_err(|_| { + let idx: BitType = self.num_qubits().try_into().map_err(|_| { PyRuntimeError::new_err( "The number of qubits in the circuit has exceeded the maximum capacity", ) @@ -284,13 +313,13 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.clbits_native.len() != self.clbits.bind(bit.py()).len() { + if self.num_clbits() != self.clbits.bind(bit.py()).len() { return Err(PyRuntimeError::new_err(concat!( "This circuit's 'clbits' list has become out of sync with the circuit data.", " Did something modify it?" ))); } - let idx: BitType = self.clbits_native.len().try_into().map_err(|_| { + let idx: BitType = self.num_clbits().try_into().map_err(|_| { PyRuntimeError::new_err( "The number of clbits in the circuit has exceeded the maximum capacity", ) @@ -460,11 +489,11 @@ impl CircuitData { ) -> PyResult<()> { let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; if qubits.is_some() { - if temp.qubits_native.len() < self.qubits_native.len() { + if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( "Replacement 'qubits' of size {:?} must contain at least {:?} bits.", - temp.qubits_native.len(), - self.qubits_native.len(), + temp.num_qubits(), + self.num_qubits(), ))); } std::mem::swap(&mut temp.qubits, &mut self.qubits); @@ -475,11 +504,11 @@ impl CircuitData { ); } if clbits.is_some() { - if temp.clbits_native.len() < self.clbits_native.len() { + if temp.num_clbits() < self.num_clbits() { return Err(PyValueError::new_err(format!( "Replacement 'clbits' of size {:?} must contain at least {:?} bits.", - temp.clbits_native.len(), - self.clbits_native.len(), + temp.num_clbits(), + self.num_clbits(), ))); } std::mem::swap(&mut temp.clbits, &mut self.clbits); diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index a157f04375a..73c5101bb46 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1919,10 +1919,10 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: edge_map.update(zip(other.qubits, dest.qubits)) else: mapped_qubits = dest.qbit_argument_conversion(qubits) - if len(mapped_qubits) != len(other.qubits): + if len(mapped_qubits) != other.num_qubits: raise CircuitError( f"Number of items in qubits parameter ({len(mapped_qubits)}) does not" - f" match number of qubits in the circuit ({len(other.qubits)})." + f" match number of qubits in the circuit ({other.num_qubits})." ) if len(set(mapped_qubits)) != len(mapped_qubits): raise CircuitError( @@ -1935,10 +1935,10 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: edge_map.update(zip(other.clbits, dest.clbits)) else: mapped_clbits = dest.cbit_argument_conversion(clbits) - if len(mapped_clbits) != len(other.clbits): + if len(mapped_clbits) != other.num_clbits: raise CircuitError( f"Number of items in clbits parameter ({len(mapped_clbits)}) does not" - f" match number of clbits in the circuit ({len(other.clbits)})." + f" match number of clbits in the circuit ({other.num_clbits})." ) if len(set(mapped_clbits)) != len(mapped_clbits): raise CircuitError( @@ -2917,7 +2917,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: self._data.add_qubit(bit) self._qubit_indices[bit] = BitLocations( - len(self._data.qubits) - 1, [(register, idx)] + self._data.num_qubits - 1, [(register, idx)] ) elif isinstance(register, ClassicalRegister): @@ -2929,7 +2929,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: self._data.add_clbit(bit) self._clbit_indices[bit] = BitLocations( - len(self._data.clbits) - 1, [(register, idx)] + self._data.num_clbits - 1, [(register, idx)] ) elif isinstance(register, list): @@ -2950,10 +2950,10 @@ def add_bits(self, bits: Iterable[Bit]) -> None: self._ancillas.append(bit) if isinstance(bit, Qubit): self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations(len(self._data.qubits) - 1, []) + self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, []) elif isinstance(bit, Clbit): self._data.add_clbit(bit) - self._clbit_indices[bit] = BitLocations(len(self._data.clbits) - 1, []) + self._clbit_indices[bit] = BitLocations(self._data.num_clbits - 1, []) else: raise CircuitError( "Expected an instance of Qubit, Clbit, or " @@ -3393,12 +3393,12 @@ def width(self) -> int: int: Width of circuit. """ - return len(self.qubits) + len(self.clbits) + return self._data.width() @property def num_qubits(self) -> int: """Return number of qubits.""" - return len(self.qubits) + return self._data.num_qubits @property def num_ancillas(self) -> int: @@ -3408,7 +3408,7 @@ def num_ancillas(self) -> int: @property def num_clbits(self) -> int: """Return number of classical bits.""" - return len(self.clbits) + return self._data.num_clbits # The stringified return type is because OrderedDict can't be subscripted before Python 3.9, and # typing.OrderedDict wasn't added until 3.7.2. It can be turned into a proper type once 3.6 @@ -3879,18 +3879,18 @@ def measure_all( else: circ = self.copy() if add_bits: - new_creg = circ._create_creg(len(circ.qubits), "meas") + new_creg = circ._create_creg(circ.num_qubits, "meas") circ.add_register(new_creg) circ.barrier() circ.measure(circ.qubits, new_creg) else: - if len(circ.clbits) < len(circ.qubits): + if circ.num_clbits < circ.num_qubits: raise CircuitError( "The number of classical bits must be equal or greater than " "the number of qubits." ) circ.barrier() - circ.measure(circ.qubits, circ.clbits[0 : len(circ.qubits)]) + circ.measure(circ.qubits, circ.clbits[0 : circ.num_qubits]) if not inplace: return circ From 9f4a474e97d356163caa6fac430ce28313e8ed51 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jun 2024 09:36:10 -0400 Subject: [PATCH 092/159] Fix version table in qpy docs (#12512) The QPY docs included a format version table that matched up the Qiskit releases to the supported QPY format versions for that release. However, a typo resulted in it being treated as a comment instead of table this commit fixes this and a small copy paste error in one of the versions listed in the table. --- qiskit/qpy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 4e7769106fc..e072536fef4 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -127,7 +127,7 @@ Qiskit (and qiskit-terra prior to Qiskit 1.0.0) release going back to the introduction of QPY in qiskit-terra 0.18.0. -.. list-table: QPY Format Version History +.. list-table:: QPY Format Version History :header-rows: 1 * - Qiskit (qiskit-terra for < 1.0.0) version @@ -138,7 +138,7 @@ - 12 * - 1.0.2 - 10, 11 - - 12 + - 11 * - 1.0.1 - 10, 11 - 11 From 767bd0733073e43b189cf63116ac124ea0868cf8 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jun 2024 15:13:59 +0100 Subject: [PATCH 093/159] Implement `__array__` for `qasm2._DefinedGate` (#12119) * Implement `__array__` for `qasm2._DefinedGate` Gates defined from OpenQASM 2 should have a well-defined matrix form (up to global phase) whenever all the constituent parts of the definition do. This manually makes such a matrix available. * Fix signature for Numpy 2.0 --- qiskit/qasm2/parse.py | 8 ++++++ .../qasm2-to-matrix-c707fe1e61b3987f.yaml | 9 +++++++ test/python/qasm2/test_structure.py | 26 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 5cb8137b5f0..30c85843a36 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -16,6 +16,8 @@ import math from typing import Iterable, Callable +import numpy as np + from qiskit.circuit import ( Barrier, CircuitInstruction, @@ -30,6 +32,7 @@ Reset, library as lib, ) +from qiskit.quantum_info import Operator from qiskit._accelerate.qasm2 import ( OpCode, UnaryOpCode, @@ -315,6 +318,11 @@ def _define(self): raise ValueError(f"received invalid bytecode to build gate: {op}") self._definition = qc + def __array__(self, dtype=None, copy=None): + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(Operator(self.definition), dtype=dtype) + # It's fiddly to implement pickling for PyO3 types (the bytecode stream), so instead if we need # to pickle ourselves, we just eagerly create the definition and pickle that. diff --git a/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml new file mode 100644 index 00000000000..84cfd1a1d35 --- /dev/null +++ b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Custom gates (those stemming from a ``gate`` statement) in imported OpenQASM 2 programs will now + have an :meth:`.Gate.to_matrix` implementation. Previously they would have no matrix definition, + meaning that roundtrips through OpenQASM 2 could needlessly lose the ability to derive the gate + matrix. Note, though, that the matrix is calculated by recursively finding the matrices of the + inner gate definitions, as :class:`.Operator` does, which might be less performant than before + the round-trip. diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index 141d3c0f8b0..22eff30b38f 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -22,6 +22,7 @@ import tempfile import ddt +import numpy as np import qiskit.qasm2 from qiskit import qpy @@ -34,6 +35,7 @@ Qubit, library as lib, ) +from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order from . import gate_builder @@ -906,6 +908,30 @@ def test_conditioned_broadcast_against_empty_register(self): ) self.assertEqual(parsed, qc) + def test_has_to_matrix(self): + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg qr[1]; + gate my_gate(a) q { + rz(a) q; + rx(pi / 2) q; + rz(-a) q; + } + my_gate(1.0) qr[0]; + """ + parsed = qiskit.qasm2.loads(program) + expected = ( + lib.RZGate(-1.0).to_matrix() + @ lib.RXGate(math.pi / 2).to_matrix() + @ lib.RZGate(1.0).to_matrix() + ) + defined_gate = parsed.data[0].operation + self.assertEqual(defined_gate.name, "my_gate") + np.testing.assert_allclose(defined_gate.to_matrix(), expected, atol=1e-14, rtol=0) + # Also test that the standard `Operator` method on the whole circuit still works. + np.testing.assert_allclose(Operator(parsed), expected, atol=1e-14, rtol=0) + class TestReset(QiskitTestCase): def test_single(self): From 778a6b24baf46285e4576e1e41b11443249f9838 Mon Sep 17 00:00:00 2001 From: Catherine Lozano Date: Thu, 6 Jun 2024 15:20:57 -0400 Subject: [PATCH 094/159] Removed deprecated bench test (#12522) --- test/benchmarks/isometry.py | 69 ------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 test/benchmarks/isometry.py diff --git a/test/benchmarks/isometry.py b/test/benchmarks/isometry.py deleted file mode 100644 index c3cf13e4d0e..00000000000 --- a/test/benchmarks/isometry.py +++ /dev/null @@ -1,69 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -# pylint: disable=missing-docstring,invalid-name,no-member -# pylint: disable=attribute-defined-outside-init -# pylint: disable=unused-argument - -from qiskit import QuantumRegister, QuantumCircuit -from qiskit.compiler import transpile -from qiskit.quantum_info.random import random_unitary -from qiskit.circuit.library.generalized_gates import Isometry - - -class IsometryTranspileBench: - params = ([0, 1, 2, 3], [3, 4, 5, 6]) - param_names = ["number of input qubits", "number of output qubits"] - - def setup(self, m, n): - q = QuantumRegister(n) - qc = QuantumCircuit(q) - if not hasattr(qc, "iso"): - raise NotImplementedError - iso = random_unitary(2**n, seed=0).data[:, 0 : 2**m] - if len(iso.shape) == 1: - iso = iso.reshape((len(iso), 1)) - iso_gate = Isometry(iso, 0, 0) - qc.append(iso_gate, q) - - self.circuit = qc - - def track_cnot_counts_after_mapping_to_ibmq_16_melbourne(self, *unused): - coupling = [ - [1, 0], - [1, 2], - [2, 3], - [4, 3], - [4, 10], - [5, 4], - [5, 6], - [5, 9], - [6, 8], - [7, 8], - [9, 8], - [9, 10], - [11, 3], - [11, 10], - [11, 12], - [12, 2], - [13, 1], - [13, 12], - ] - circuit = transpile( - self.circuit, - basis_gates=["u1", "u3", "u2", "cx"], - coupling_map=coupling, - seed_transpiler=0, - ) - counts = circuit.count_ops() - cnot_count = counts.get("cx", 0) - return cnot_count From 1798c3f20868225f5416186e25b8e20e8b29199c Mon Sep 17 00:00:00 2001 From: Catherine Lozano Date: Thu, 6 Jun 2024 15:23:35 -0400 Subject: [PATCH 095/159] fixing deprecated methods and incorrect dag state (#12513) * changed dag state for layout_2q and apply_layout to dag before layout in setup * Removed CX tests using depreciated cx functions * fixed formatting --- test/benchmarks/mapping_passes.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/benchmarks/mapping_passes.py b/test/benchmarks/mapping_passes.py index 180925905d2..4f87323f33a 100644 --- a/test/benchmarks/mapping_passes.py +++ b/test/benchmarks/mapping_passes.py @@ -124,12 +124,12 @@ def time_dense_layout(self, _, __): def time_layout_2q_distance(self, _, __): layout = Layout2qDistance(self.coupling_map) layout.property_set["layout"] = self.layout - layout.run(self.dag) + layout.run(self.enlarge_dag) def time_apply_layout(self, _, __): layout = ApplyLayout() layout.property_set["layout"] = self.layout - layout.run(self.dag) + layout.run(self.enlarge_dag) def time_full_ancilla_allocation(self, _, __): ancilla = FullAncillaAllocation(self.coupling_map) @@ -232,12 +232,6 @@ def setup(self, n_qubits, depth): self.backend_props = Fake20QV1().properties() self.routed_dag = StochasticSwap(self.coupling_map, seed=42).run(self.dag) - def time_cxdirection(self, _, __): - CXDirection(self.coupling_map).run(self.routed_dag) - - def time_check_cx_direction(self, _, __): - CheckCXDirection(self.coupling_map).run(self.routed_dag) - def time_gate_direction(self, _, __): GateDirection(self.coupling_map).run(self.routed_dag) From d1c8404dcadf487d26f048aa4ba4c148767349dd Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 6 Jun 2024 16:29:23 -0400 Subject: [PATCH 096/159] [DAGCircuit Oxidation] Refactor bit management in `CircuitData` (#12372) * Checkpoint before rebase. * Refactor bit management in CircuitData. * Revert changes not for this PR. * Add doc comment for InstructionPacker, rename Interner::lookup. * Add CircuitData::map_* and refactor. * Fix merge issue. * CircuitInstruction::new returns Self. * Use unbind. * Make bit types pub. * Fix merge. --- crates/circuit/src/bit_data.rs | 192 ++++++++++++ crates/circuit/src/circuit_data.rs | 338 +++++++--------------- crates/circuit/src/circuit_instruction.rs | 42 ++- crates/circuit/src/intern_context.rs | 71 ----- crates/circuit/src/interner.rs | 125 ++++++++ crates/circuit/src/lib.rs | 37 ++- crates/circuit/src/packed_instruction.rs | 25 ++ 7 files changed, 513 insertions(+), 317 deletions(-) create mode 100644 crates/circuit/src/bit_data.rs delete mode 100644 crates/circuit/src/intern_context.rs create mode 100644 crates/circuit/src/interner.rs create mode 100644 crates/circuit/src/packed_instruction.rs diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs new file mode 100644 index 00000000000..7964ec186e0 --- /dev/null +++ b/crates/circuit/src/bit_data.rs @@ -0,0 +1,192 @@ +// 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 crate::BitType; +use hashbrown::HashMap; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyList; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; + +/// Private wrapper for Python-side Bit instances that implements +/// [Hash] and [Eq], allowing them to be used in Rust hash-based +/// sets and maps. +/// +/// Python's `hash()` is called on the wrapped Bit instance during +/// construction and returned from Rust's [Hash] trait impl. +/// The impl of [PartialEq] first compares the native Py pointers +/// to determine equality. If these are not equal, only then does +/// it call `repr()` on both sides, which has a significant +/// performance advantage. +#[derive(Clone, Debug)] +struct BitAsKey { + /// Python's `hash()` of the wrapped instance. + hash: isize, + /// The wrapped instance. + bit: PyObject, +} + +impl BitAsKey { + pub fn new(bit: &Bound) -> Self { + BitAsKey { + // This really shouldn't fail, but if it does, + // we'll just use 0. + hash: bit.hash().unwrap_or(0), + bit: bit.clone().unbind(), + } + } +} + +impl Hash for BitAsKey { + fn hash(&self, state: &mut H) { + state.write_isize(self.hash); + } +} + +impl PartialEq for BitAsKey { + fn eq(&self, other: &Self) -> bool { + self.bit.is(&other.bit) + || Python::with_gil(|py| { + self.bit + .bind(py) + .repr() + .unwrap() + .eq(other.bit.bind(py).repr().unwrap()) + .unwrap() + }) + } +} + +impl Eq for BitAsKey {} + +#[derive(Clone, Debug)] +pub(crate) struct BitData { + /// The public field name (i.e. `qubits` or `clbits`). + description: String, + /// Registered Python bits. + bits: Vec, + /// Maps Python bits to native type. + indices: HashMap, + /// The bits registered, cached as a PyList. + cached: Py, +} + +pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); + +impl BitData +where + T: From + Copy, + BitType: From, +{ + pub fn new(py: Python<'_>, description: String) -> Self { + BitData { + description, + bits: Vec::new(), + indices: HashMap::new(), + cached: PyList::empty_bound(py).unbind(), + } + } + + /// Gets the number of bits. + pub fn len(&self) -> usize { + self.bits.len() + } + + /// Gets a reference to the underlying vector of Python bits. + #[inline] + pub fn bits(&self) -> &Vec { + &self.bits + } + + /// Gets a reference to the cached Python list, maintained by + /// this instance. + #[inline] + pub fn cached(&self) -> &Py { + &self.cached + } + + /// Finds the native bit index of the given Python bit. + #[inline] + pub fn find(&self, bit: &Bound) -> Option { + self.indices.get(&BitAsKey::new(bit)).copied() + } + + /// Map the provided Python bits to their native indices. + /// An error is returned if any bit is not registered. + pub fn map_bits<'py>( + &self, + bits: impl IntoIterator>, + ) -> Result, BitNotFoundError<'py>> { + let v: Result, _> = bits + .into_iter() + .map(|b| { + self.indices + .get(&BitAsKey::new(&b)) + .copied() + .ok_or_else(|| BitNotFoundError(b)) + }) + .collect(); + v.map(|x| x.into_iter()) + } + + /// Map the provided native indices to the corresponding Python + /// bit instances. + /// Panics if any of the indices are out of range. + pub fn map_indices(&self, bits: &[T]) -> impl Iterator> + ExactSizeIterator { + let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect(); + v.into_iter() + } + + /// Gets the Python bit corresponding to the given native + /// bit index. + #[inline] + pub fn get(&self, index: T) -> Option<&PyObject> { + self.bits.get(>::from(index) as usize) + } + + /// Adds a new Python bit. + pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { + if self.bits.len() != self.cached.bind(bit.py()).len() { + return Err(PyRuntimeError::new_err( + format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description) + )); + } + let idx: BitType = self.bits.len().try_into().map_err(|_| { + PyRuntimeError::new_err(format!( + "The number of {} in the circuit has exceeded the maximum capacity", + self.description + )) + })?; + if self + .indices + .try_insert(BitAsKey::new(bit), idx.into()) + .is_ok() + { + self.bits.push(bit.into_py(py)); + self.cached.bind(py).append(bit)?; + } else if strict { + return Err(PyValueError::new_err(format!( + "Existing bit {:?} cannot be re-added in strict mode.", + bit + ))); + } + Ok(()) + } + + /// Called during Python garbage collection, only!. + /// Note: INVALIDATES THIS INSTANCE. + pub fn dispose(&mut self) { + self.indices.clear(); + self.bits.clear(); + } +} diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index d8eca9ef854..fbb7c06fc89 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1,6 +1,6 @@ // This code is part of Qiskit. // -// (C) Copyright IBM 2023 +// (C) Copyright IBM 2023, 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 @@ -10,76 +10,17 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use crate::bit_data::{BitData, BitNotFoundError}; use crate::circuit_instruction::CircuitInstruction; -use crate::intern_context::{BitType, IndexType, InternContext}; -use crate::SliceOrInt; +use crate::interner::{CacheFullError, IndexedInterner, Interner, InternerKey}; +use crate::packed_instruction::PackedInstruction; +use crate::{Clbit, Qubit, SliceOrInt}; -use hashbrown::HashMap; use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; -use std::hash::{Hash, Hasher}; - -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -struct PackedInstruction { - /// The Python-side operation instance. - op: PyObject, - /// The index under which the interner has stored `qubits`. - qubits_id: IndexType, - /// The index under which the interner has stored `clbits`. - clbits_id: IndexType, -} - -/// Private wrapper for Python-side Bit instances that implements -/// [Hash] and [Eq], allowing them to be used in Rust hash-based -/// sets and maps. -/// -/// Python's `hash()` is called on the wrapped Bit instance during -/// construction and returned from Rust's [Hash] trait impl. -/// The impl of [PartialEq] first compares the native Py pointers -/// to determine equality. If these are not equal, only then does -/// it call `repr()` on both sides, which has a significant -/// performance advantage. -#[derive(Clone, Debug)] -struct BitAsKey { - /// Python's `hash()` of the wrapped instance. - hash: isize, - /// The wrapped instance. - bit: PyObject, -} - -impl BitAsKey { - fn new(bit: &Bound) -> PyResult { - Ok(BitAsKey { - hash: bit.hash()?, - bit: bit.into_py(bit.py()), - }) - } -} - -impl Hash for BitAsKey { - fn hash(&self, state: &mut H) { - state.write_isize(self.hash); - } -} - -impl PartialEq for BitAsKey { - fn eq(&self, other: &Self) -> bool { - self.bit.is(&other.bit) - || Python::with_gil(|py| { - self.bit - .bind(py) - .repr() - .unwrap() - .eq(other.bit.bind(py).repr().unwrap()) - .unwrap() - }) - } -} - -impl Eq for BitAsKey {} +use std::mem; /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning @@ -136,22 +77,29 @@ impl Eq for BitAsKey {} pub struct CircuitData { /// The packed instruction listing. data: Vec, - /// The intern context used to intern instruction bits. - intern_context: InternContext, - /// The qubits registered (e.g. through :meth:`~.CircuitData.add_qubit`). - qubits_native: Vec, - /// The clbits registered (e.g. through :meth:`~.CircuitData.add_clbit`). - clbits_native: Vec, - /// Map of :class:`.Qubit` instances to their index in - /// :attr:`.CircuitData.qubits`. - qubit_indices_native: HashMap, - /// Map of :class:`.Clbit` instances to their index in - /// :attr:`.CircuitData.clbits`. - clbit_indices_native: HashMap, - /// The qubits registered, cached as a ``list[Qubit]``. - qubits: Py, - /// The clbits registered, cached as a ``list[Clbit]``. - clbits: Py, + /// The cache used to intern instruction bits. + qargs_interner: IndexedInterner>, + /// The cache used to intern instruction bits. + cargs_interner: IndexedInterner>, + /// Qubits registered in the circuit. + qubits: BitData, + /// Clbits registered in the circuit. + clbits: BitData, +} + +impl<'py> From> for PyErr { + fn from(error: BitNotFoundError) -> Self { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + error.0 + )) + } +} + +impl From for PyErr { + fn from(_: CacheFullError) -> Self { + PyRuntimeError::new_err("The bit operands cache is full!") + } } #[pymethods] @@ -167,13 +115,10 @@ impl CircuitData { ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), - intern_context: InternContext::new(), - qubits_native: Vec::new(), - clbits_native: Vec::new(), - qubit_indices_native: HashMap::new(), - clbit_indices_native: HashMap::new(), - qubits: PyList::empty_bound(py).unbind(), - clbits: PyList::empty_bound(py).unbind(), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), }; if let Some(qubits) = qubits { for bit in qubits.iter()? { @@ -197,8 +142,8 @@ impl CircuitData { let args = { let self_ = self_.borrow(); ( - self_.qubits.clone_ref(py), - self_.clbits.clone_ref(py), + self_.qubits.cached().clone_ref(py), + self_.clbits.cached().clone_ref(py), None::<()>, self_.data.len(), ) @@ -217,7 +162,7 @@ impl CircuitData { /// list(:class:`.Qubit`): The current sequence of registered qubits. #[getter] pub fn qubits(&self, py: Python<'_>) -> Py { - self.qubits.clone_ref(py) + self.qubits.cached().clone_ref(py) } /// Return the number of qubits. This is equivalent to the length of the list returned by @@ -227,7 +172,7 @@ impl CircuitData { /// int: The number of qubits. #[getter] pub fn num_qubits(&self) -> usize { - self.qubits_native.len() + self.qubits.len() } /// Returns the current sequence of registered :class:`.Clbit` @@ -242,7 +187,7 @@ impl CircuitData { /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter] pub fn clbits(&self, py: Python<'_>) -> Py { - self.clbits.clone_ref(py) + self.clbits.cached().clone_ref(py) } /// Return the number of clbits. This is equivalent to the length of the list returned by @@ -252,7 +197,7 @@ impl CircuitData { /// int: The number of clbits. #[getter] pub fn num_clbits(&self) -> usize { - self.clbits_native.len() + self.clbits.len() } /// Return the width of the circuit. This is the number of qubits plus the @@ -275,31 +220,7 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.num_qubits() != self.qubits.bind(bit.py()).len() { - return Err(PyRuntimeError::new_err(concat!( - "This circuit's 'qubits' list has become out of sync with the circuit data.", - " Did something modify it?" - ))); - } - let idx: BitType = self.num_qubits().try_into().map_err(|_| { - PyRuntimeError::new_err( - "The number of qubits in the circuit has exceeded the maximum capacity", - ) - })?; - if self - .qubit_indices_native - .try_insert(BitAsKey::new(bit)?, idx) - .is_ok() - { - self.qubits_native.push(bit.into_py(py)); - self.qubits.bind(py).append(bit)?; - } else if strict { - return Err(PyValueError::new_err(format!( - "Existing bit {:?} cannot be re-added in strict mode.", - bit - ))); - } - Ok(()) + self.qubits.add(py, bit, strict) } /// Registers a :class:`.Clbit` instance. @@ -313,31 +234,7 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.num_clbits() != self.clbits.bind(bit.py()).len() { - return Err(PyRuntimeError::new_err(concat!( - "This circuit's 'clbits' list has become out of sync with the circuit data.", - " Did something modify it?" - ))); - } - let idx: BitType = self.num_clbits().try_into().map_err(|_| { - PyRuntimeError::new_err( - "The number of clbits in the circuit has exceeded the maximum capacity", - ) - })?; - if self - .clbit_indices_native - .try_insert(BitAsKey::new(bit)?, idx) - .is_ok() - { - self.clbits_native.push(bit.into_py(py)); - self.clbits.bind(py).append(bit)?; - } else if strict { - return Err(PyValueError::new_err(format!( - "Existing bit {:?} cannot be re-added in strict mode.", - bit - ))); - } - Ok(()) + self.clbits.add(py, bit, strict) } /// Performs a shallow copy. @@ -347,12 +244,13 @@ impl CircuitData { pub fn copy(&self, py: Python<'_>) -> PyResult { let mut res = CircuitData::new( py, - Some(self.qubits.bind(py)), - Some(self.clbits.bind(py)), + Some(self.qubits.cached().bind(py)), + Some(self.clbits.cached().bind(py)), None, 0, )?; - res.intern_context = self.intern_context.clone(); + res.qargs_interner = self.qargs_interner.clone(); + res.cargs_interner = self.cargs_interner.clone(); res.data.clone_from(&self.data); Ok(res) } @@ -376,11 +274,11 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.intern_context.lookup(inst.qubits_id).iter() { - qubits.add(self.qubits_native[*b as usize].clone_ref(py))?; + for b in self.qargs_interner.intern(inst.qubits_id).value.iter() { + qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.intern_context.lookup(inst.clbits_id).iter() { - clbits.add(self.clbits_native[*b as usize].clone_ref(py))?; + for b in self.cargs_interner.intern(inst.clbits_id).value.iter() { + clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -496,12 +394,7 @@ impl CircuitData { self.num_qubits(), ))); } - std::mem::swap(&mut temp.qubits, &mut self.qubits); - std::mem::swap(&mut temp.qubits_native, &mut self.qubits_native); - std::mem::swap( - &mut temp.qubit_indices_native, - &mut self.qubit_indices_native, - ); + mem::swap(&mut temp.qubits, &mut self.qubits); } if clbits.is_some() { if temp.num_clbits() < self.num_clbits() { @@ -511,12 +404,7 @@ impl CircuitData { self.num_clbits(), ))); } - std::mem::swap(&mut temp.clbits, &mut self.clbits); - std::mem::swap(&mut temp.clbits_native, &mut self.clbits_native); - std::mem::swap( - &mut temp.clbit_indices_native, - &mut self.clbit_indices_native, - ); + mem::swap(&mut temp.clbits, &mut self.clbits); } Ok(()) } @@ -536,7 +424,17 @@ impl CircuitData { ) -> PyResult> { let index = self_.convert_py_index(index)?; if let Some(inst) = self_.data.get(index) { - self_.unpack(py, inst) + let qubits = self_.qargs_interner.intern(inst.qubits_id); + let clbits = self_.cargs_interner.intern(inst.clbits_id); + Py::new( + py, + CircuitInstruction::new( + py, + inst.op.clone_ref(py), + self_.qubits.map_indices(qubits.value), + self_.clbits.map_indices(clbits.value), + ), + ) } else { Err(PyIndexError::new_err(format!( "No element at index {:?} in circuit data", @@ -637,7 +535,7 @@ impl CircuitData { let index = self.convert_py_index(index)?; let value: PyRef = value.extract()?; let mut packed = self.pack(py, value)?; - std::mem::swap(&mut packed, &mut self.data[index]); + mem::swap(&mut packed, &mut self.data[index]); Ok(()) } } @@ -676,28 +574,38 @@ impl CircuitData { self.data.reserve(other.data.len()); for inst in other.data.iter() { let qubits = other - .intern_context - .lookup(inst.qubits_id) + .qargs_interner + .intern(inst.qubits_id) + .value .iter() .map(|b| { - Ok(self.qubit_indices_native - [&BitAsKey::new(other.qubits_native[*b as usize].bind(py))?]) + Ok(self + .qubits + .find(other.qubits.get(*b).unwrap().bind(py)) + .unwrap()) }) - .collect::>>()?; + .collect::>>()?; let clbits = other - .intern_context - .lookup(inst.clbits_id) + .cargs_interner + .intern(inst.clbits_id) + .value .iter() .map(|b| { - Ok(self.clbit_indices_native - [&BitAsKey::new(other.clbits_native[*b as usize].bind(py))?]) + Ok(self + .clbits + .find(other.clbits.get(*b).unwrap().bind(py)) + .unwrap()) }) - .collect::>>()?; + .collect::>>()?; + let qubits_id = + Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; + let clbits_id = + Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; self.data.push(PackedInstruction { op: inst.op.clone_ref(py), - qubits_id: self.intern_context.intern(qubits)?, - clbits_id: self.intern_context.intern(clbits)?, + qubits_id: qubits_id.index, + clbits_id: clbits_id.index, }); } return Ok(()); @@ -751,7 +659,7 @@ impl CircuitData { for packed in self.data.iter() { visit.call(&packed.op)?; } - for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) { + for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { visit.call(bit)?; } @@ -759,18 +667,16 @@ impl CircuitData { // There's no need to visit the native Rust data // structures used for internal tracking: the only Python // references they contain are to the bits in these lists! - visit.call(&self.qubits)?; - visit.call(&self.clbits)?; + visit.call(self.qubits.cached())?; + visit.call(self.clbits.cached())?; Ok(()) } fn __clear__(&mut self) { // Clear anything that could have a reference cycle. self.data.clear(); - self.qubits_native.clear(); - self.clbits_native.clear(); - self.qubit_indices_native.clear(); - self.clbit_indices_native.clear(); + self.qubits.dispose(); + self.clbits.dispose(); } } @@ -824,61 +730,23 @@ impl CircuitData { Ok(index as usize) } - /// Returns a [PackedInstruction] containing the original operation - /// of `elem` and [InternContext] indices of its `qubits` and `clbits` - /// fields. fn pack( &mut self, - py: Python<'_>, - inst: PyRef, + py: Python, + value: PyRef, ) -> PyResult { - let mut interned_bits = - |indices: &HashMap, bits: &Bound| -> PyResult { - let args = bits - .into_iter() - .map(|b| { - let key = BitAsKey::new(&b)?; - indices.get(&key).copied().ok_or_else(|| { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - b - )) - }) - }) - .collect::>>()?; - self.intern_context.intern(args) - }; + let qubits = Interner::intern( + &mut self.qargs_interner, + InternerKey::Value(self.qubits.map_bits(value.qubits.bind(py))?.collect()), + )?; + let clbits = Interner::intern( + &mut self.cargs_interner, + InternerKey::Value(self.clbits.map_bits(value.clbits.bind(py))?.collect()), + )?; Ok(PackedInstruction { - op: inst.operation.clone_ref(py), - qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, - clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, + op: value.operation.clone_ref(py), + qubits_id: qubits.index, + clbits_id: clbits.index, }) } - - fn unpack(&self, py: Python<'_>, inst: &PackedInstruction) -> PyResult> { - Py::new( - py, - CircuitInstruction { - operation: inst.op.clone_ref(py), - qubits: PyTuple::new_bound( - py, - self.intern_context - .lookup(inst.qubits_id) - .iter() - .map(|i| self.qubits_native[*i as usize].clone_ref(py)) - .collect::>(), - ) - .unbind(), - clbits: PyTuple::new_bound( - py, - self.intern_context - .lookup(inst.clbits_id) - .iter() - .map(|i| self.clbits_native[*i as usize].clone_ref(py)) - .collect::>(), - ) - .unbind(), - }, - ) - } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 86bd2e69c11..ac61ae81a61 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -63,15 +63,36 @@ pub struct CircuitInstruction { pub clbits: Py, } +impl CircuitInstruction { + pub fn new( + py: Python, + operation: PyObject, + qubits: impl IntoIterator, + clbits: impl IntoIterator, + ) -> Self + where + T1: ToPyObject, + T2: ToPyObject, + U1: ExactSizeIterator, + U2: ExactSizeIterator, + { + CircuitInstruction { + operation, + qubits: PyTuple::new_bound(py, qubits).unbind(), + clbits: PyTuple::new_bound(py, clbits).unbind(), + } + } +} + #[pymethods] impl CircuitInstruction { #[new] - pub fn new( + pub fn py_new( py: Python<'_>, operation: PyObject, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult { + ) -> PyResult> { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { match seq { None => Ok(PyTuple::empty_bound(py).unbind()), @@ -95,11 +116,14 @@ impl CircuitInstruction { } } - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - }) + Py::new( + py, + CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + }, + ) } /// Returns a shallow copy. @@ -120,8 +144,8 @@ impl CircuitInstruction { operation: Option, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult { - CircuitInstruction::new( + ) -> PyResult> { + CircuitInstruction::py_new( py, operation.unwrap_or_else(|| self.operation.clone_ref(py)), Some(qubits.unwrap_or_else(|| self.qubits.bind(py))), diff --git a/crates/circuit/src/intern_context.rs b/crates/circuit/src/intern_context.rs deleted file mode 100644 index 0c8b596e6dd..00000000000 --- a/crates/circuit/src/intern_context.rs +++ /dev/null @@ -1,71 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2023 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -use hashbrown::HashMap; -use pyo3::exceptions::PyRuntimeError; -use pyo3::PyResult; -use std::sync::Arc; - -pub type IndexType = u32; -pub type BitType = u32; - -/// A Rust-only data structure (not a pyclass!) for interning -/// `Vec`. -/// -/// Takes ownership of vectors given to [InternContext.intern] -/// and returns an [IndexType] index that can be used to look up -/// an _equivalent_ sequence by reference via [InternContext.lookup]. -#[derive(Clone, Debug)] -pub struct InternContext { - slots: Vec>>, - slot_lookup: HashMap>, IndexType>, -} - -impl InternContext { - pub fn new() -> Self { - InternContext { - slots: Vec::new(), - slot_lookup: HashMap::new(), - } - } - - /// Takes `args` by reference and returns an index that can be used - /// to obtain a reference to an equivalent sequence of `BitType` by - /// calling [CircuitData.lookup]. - pub fn intern(&mut self, args: Vec) -> PyResult { - if let Some(slot_idx) = self.slot_lookup.get(&args) { - return Ok(*slot_idx); - } - - let args = Arc::new(args); - let slot_idx: IndexType = self - .slots - .len() - .try_into() - .map_err(|_| PyRuntimeError::new_err("InternContext capacity exceeded!"))?; - self.slots.push(args.clone()); - self.slot_lookup.insert_unique_unchecked(args, slot_idx); - Ok(slot_idx) - } - - /// Returns the sequence corresponding to `slot_idx`, which must - /// be a value returned by [InternContext.intern]. - pub fn lookup(&self, slot_idx: IndexType) -> &[BitType] { - self.slots.get(slot_idx as usize).unwrap() - } -} - -impl Default for InternContext { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs new file mode 100644 index 00000000000..42667570205 --- /dev/null +++ b/crates/circuit/src/interner.rs @@ -0,0 +1,125 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023, 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 hashbrown::HashMap; +use pyo3::{IntoPy, PyObject, Python}; +use std::hash::Hash; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug)] +pub struct Index(u32); + +pub enum InternerKey { + Index(Index), + Value(T), +} + +impl From for InternerKey { + fn from(value: Index) -> Self { + InternerKey::Index(value) + } +} + +pub struct InternerValue<'a, T> { + pub index: Index, + pub value: &'a T, +} + +impl IntoPy for Index { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0.into_py(py) + } +} + +pub struct CacheFullError; + +/// An append-only data structure for interning generic +/// Rust types. +#[derive(Clone, Debug)] +pub struct IndexedInterner { + entries: Vec>, + index_lookup: HashMap, Index>, +} + +pub trait Interner { + type Key; + type Output; + + /// Takes ownership of the provided key and returns the interned + /// type. + fn intern(self, value: Self::Key) -> Self::Output; +} + +impl<'a, T> Interner for &'a IndexedInterner { + type Key = Index; + type Output = InternerValue<'a, T>; + + fn intern(self, index: Index) -> Self::Output { + let value = self.entries.get(index.0 as usize).unwrap(); + InternerValue { + index, + value: value.as_ref(), + } + } +} + +impl<'a, T> Interner for &'a mut IndexedInterner +where + T: Eq + Hash, +{ + type Key = InternerKey; + type Output = Result, CacheFullError>; + + fn intern(self, key: Self::Key) -> Self::Output { + match key { + InternerKey::Index(index) => { + let value = self.entries.get(index.0 as usize).unwrap(); + Ok(InternerValue { + index, + value: value.as_ref(), + }) + } + InternerKey::Value(value) => { + if let Some(index) = self.index_lookup.get(&value).copied() { + Ok(InternerValue { + index, + value: self.entries.get(index.0 as usize).unwrap(), + }) + } else { + let args = Arc::new(value); + let index: Index = + Index(self.entries.len().try_into().map_err(|_| CacheFullError)?); + self.entries.push(args.clone()); + Ok(InternerValue { + index, + value: self.index_lookup.insert_unique_unchecked(args, index).0, + }) + } + } + } + } +} + +impl IndexedInterner { + pub fn new() -> Self { + IndexedInterner { + entries: Vec::new(), + index_lookup: HashMap::new(), + } + } +} + +impl Default for IndexedInterner { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index c186c4243e9..90f2b7c7f07 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -1,6 +1,6 @@ // This code is part of Qiskit. // -// (C) Copyright IBM 2023 +// (C) Copyright IBM 2023, 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 @@ -13,7 +13,10 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; -pub mod intern_context; + +mod bit_data; +mod interner; +mod packed_instruction; use pyo3::prelude::*; use pyo3::types::PySlice; @@ -28,6 +31,36 @@ pub enum SliceOrInt<'a> { Slice(Bound<'a, PySlice>), } +pub type BitType = u32; +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Qubit(BitType); +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Clbit(BitType); + +impl From for Qubit { + fn from(value: BitType) -> Self { + Qubit(value) + } +} + +impl From for BitType { + fn from(value: Qubit) -> Self { + value.0 + } +} + +impl From for Clbit { + fn from(value: BitType) -> Self { + Clbit(value) + } +} + +impl From for BitType { + fn from(value: Clbit) -> Self { + value.0 + } +} + #[pymodule] pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs new file mode 100644 index 00000000000..0c793f2b640 --- /dev/null +++ b/crates/circuit/src/packed_instruction.rs @@ -0,0 +1,25 @@ +// 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 crate::interner::Index; +use pyo3::prelude::*; + +/// Private type used to store instructions with interned arg lists. +#[derive(Clone, Debug)] +pub(crate) struct PackedInstruction { + /// The Python-side operation instance. + pub op: PyObject, + /// The index under which the interner has stored `qubits`. + pub qubits_id: Index, + /// The index under which the interner has stored `clbits`. + pub clbits_id: Index, +} From 92e78ef72a0f8aaaa57c4ca7f97e7646ace57c64 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jun 2024 21:53:46 +0100 Subject: [PATCH 097/159] Bump pylint to 3.2.2 (#12520) * Bump pylint to 3.2.2 This upgrades pylint to a version compatible with Python 3.8 through 3.12, like Qiskit. There are a couple of false positives (pylint is incorrect about its `use-yield-from` in the cases it flags), and has included a couple of new stylistic opinions that are not necessary to enforce. The `possibly-used-before-assignment` lint is quite possibly a good one, but there are far too many instances in the Qiskit codebase right now to fix, whereas our current version of pylint is preventing us from running it with Python 3.12. * Update requirements-dev.txt Co-authored-by: Pierre Sassoulas * Remove suppressions fixed in pylint 3.2.3 --------- Co-authored-by: Pierre Sassoulas Co-authored-by: Matthew Treinish --- pyproject.toml | 2 ++ qiskit/transpiler/passmanager.py | 4 ++-- qiskit/visualization/circuit/text.py | 8 +------- requirements-dev.txt | 4 ++-- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35a14a5524b..6e57fa53a7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,6 +213,7 @@ disable = [ "docstring-first-line-empty", # relax docstring style "import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages "nested-min-max", # this gives false equivalencies if implemented for the current lint version + "consider-using-max-builtin", "consider-using-min-builtin", # unnecessary stylistic opinion # TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale @@ -222,6 +223,7 @@ disable = [ "no-member", "no-value-for-parameter", "not-context-manager", + "possibly-used-before-assignment", "unexpected-keyword-arg", "unnecessary-dunder-call", "unnecessary-lambda-assignment", diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index bb1344e34cb..96c0be11b44 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -18,7 +18,7 @@ import re from collections.abc import Iterator, Iterable, Callable from functools import wraps -from typing import Union, List, Any +from typing import Union, List, Any, TypeVar from qiskit.circuit import QuantumCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -31,7 +31,7 @@ from .exceptions import TranspilerError from .layout import TranspileLayout, Layout -_CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] +_CircuitsT = TypeVar("_CircuitsT", bound=Union[List[QuantumCircuit], QuantumCircuit]) class PassManager(BasePassManager): diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index c2846da0b75..bec1ccf4a3e 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -739,13 +739,7 @@ def __init__( self._wire_map = {} self.cregbundle = cregbundle - if encoding: - self.encoding = encoding - else: - if sys.stdout.encoding: - self.encoding = sys.stdout.encoding - else: - self.encoding = "utf8" + self.encoding = encoding or sys.stdout.encoding or "utf8" self._nest_depth = 0 # nesting depth for control flow ops self._expr_text = "" # expression text to display diff --git a/requirements-dev.txt b/requirements-dev.txt index c75237e77ed..7c5a909bd39 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,8 +17,8 @@ black[jupyter]~=24.1 # # These versions are pinned precisely because pylint frequently includes new # on-by-default lint failures in new versions, which breaks our CI. -astroid==2.14.2 -pylint==2.16.2 +astroid==3.2.2 +pylint==3.2.3 ruff==0.0.267 From 90f09dab70a310237374c95da840c15ba9ea13bb Mon Sep 17 00:00:00 2001 From: Catherine Lozano Date: Thu, 6 Jun 2024 19:28:33 -0400 Subject: [PATCH 098/159] depreciated U3 gate(qc.U3) changed to qc.U (#12514) --- test/benchmarks/random_circuit_hex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/benchmarks/random_circuit_hex.py b/test/benchmarks/random_circuit_hex.py index 952c651df9f..92f1cb5843b 100644 --- a/test/benchmarks/random_circuit_hex.py +++ b/test/benchmarks/random_circuit_hex.py @@ -41,7 +41,7 @@ def make_circuit_ring(nq, depth, seed): for i in range(nq): # round of single-qubit unitaries u = random_unitary(2, seed).data angles = decomposer.angles(u) - qc.u3(angles[0], angles[1], angles[2], q[i]) + qc.u(angles[0], angles[1], angles[2], q[i]) # insert the final measurements qcm = copy.deepcopy(qc) From 72f09adf7e434dbdfc84b6a2cf12af044f99cf8d Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 6 Jun 2024 19:29:08 -0400 Subject: [PATCH 099/159] Avoid exception in `Target.has_calibration` for instruction without properties (#12526) `Target.add_instruction` allows passing `None` in place of an `InstructionProperties` instance. In this case, there will be no `_calibration` attribute, so the `getattr` call properties needs to provide a default value. --- qiskit/transpiler/target.py | 2 +- ...ration-no-properties-f3be18f2d58f330a.yaml | 7 ++++++ test/python/transpiler/test_target.py | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index f7b5227c026..53daa4ccbe6 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -892,7 +892,7 @@ def has_calibration( return False if qargs not in self._gate_map[operation_name]: return False - return getattr(self._gate_map[operation_name][qargs], "_calibration") is not None + return getattr(self._gate_map[operation_name][qargs], "_calibration", None) is not None def get_calibration( self, diff --git a/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml b/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml new file mode 100644 index 00000000000..07970679722 --- /dev/null +++ b/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:`.Target.has_calibration` has been updated so that it does not raise + an exception for an instruction that has been added to the target with + ``None`` for its instruction properties. Fixes + `#12525 `__. diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 646b29dd383..f63ed5061cc 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1366,6 +1366,31 @@ def test_get_empty_target_calibration(self): self.assertIsNone(target["x"][(0,)].calibration) + def test_has_calibration(self): + target = Target() + properties = { + (0,): InstructionProperties(duration=100, error=0.1), + (1,): None, + } + target.add_instruction(XGate(), properties) + + # Test false for properties with no calibration + self.assertFalse(target.has_calibration("x", (0,))) + # Test false for no properties + self.assertFalse(target.has_calibration("x", (1,))) + + properties = { + (0,): InstructionProperties( + duration=self.custom_sx_q0.duration, + error=None, + calibration=self.custom_sx_q0, + ) + } + target.add_instruction(SXGate(), properties) + + # Test true for properties with calibration + self.assertTrue(target.has_calibration("sx", (0,))) + def test_loading_legacy_ugate_instmap(self): # This is typical IBM backend situation. # IBM provider used to have u1, u2, u3 in the basis gates and From d18a74cd45922f4960a9eb90ceef89f9c6c184e5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 7 Jun 2024 12:49:18 +0100 Subject: [PATCH 100/159] Fix `QuantumCircuit.depth` with zero-operands and `Expr` nodes (#12429) This causes `QuantumCircuit.depth` to correctly handle cases where a circuit instruction has zero operands (such as `GlobalPhaseGate`), and to treat classical bits and real-time variables used inside `Expr` conditions as part of the depth calculations. This is in line with `DAGCircuit`. This commit still does not add the same `recurse` argument from `DAGCircuit.depth`, because the arguments for not adding it to `QuantumCircuit.depth` at the time still hold; there is no clear meaning to it for general control flow from a user's perspective, and it was only added to the `DAGCircuit` methods because there it is more of a proxy for optimising over all possible inner blocks. --- qiskit/circuit/quantumcircuit.py | 82 ++++++++----------- .../fix-qc-depth-0q-cdcc9aa14e237e68.yaml | 8 ++ .../python/circuit/test_circuit_properties.py | 55 ++++++++++++- 3 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 73c5101bb46..ea4361fd825 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -45,7 +45,7 @@ from qiskit.circuit.exceptions import CircuitError from . import _classical_resource_map from ._utils import sort_parameters -from .controlflow import ControlFlowOp +from .controlflow import ControlFlowOp, _builder_utils from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder @@ -3307,6 +3307,9 @@ def depth( ) -> int: """Return circuit depth (i.e., length of critical path). + .. warning:: + This operation is not well defined if the circuit contains control-flow operations. + Args: filter_function: A function to decide which instructions count to increase depth. Should take as a single positional input a :class:`CircuitInstruction`. @@ -3332,59 +3335,40 @@ def depth( assert qc.depth(lambda instr: len(instr.qubits) > 1) == 1 """ - # Assign each bit in the circuit a unique integer - # to index into op_stack. - bit_indices: dict[Qubit | Clbit, int] = { - bit: idx for idx, bit in enumerate(self.qubits + self.clbits) + obj_depths = { + obj: 0 for objects in (self.qubits, self.clbits, self.iter_vars()) for obj in objects } - # If no bits, return 0 - if not bit_indices: - return 0 + def update_from_expr(objects, node): + for var in expr.iter_vars(node): + if var.standalone: + objects.add(var) + else: + objects.update(_builder_utils.node_resources(var).clbits) - # A list that holds the height of each qubit - # and classical bit. - op_stack = [0] * len(bit_indices) - - # Here we are playing a modified version of - # Tetris where we stack gates, but multi-qubit - # gates, or measurements have a block for each - # qubit or cbit that are connected by a virtual - # line so that they all stacked at the same depth. - # Conditional gates act on all cbits in the register - # they are conditioned on. - # The max stack height is the circuit depth. for instruction in self._data: - levels = [] - reg_ints = [] - for ind, reg in enumerate(instruction.qubits + instruction.clbits): - # Add to the stacks of the qubits and - # cbits used in the gate. - reg_ints.append(bit_indices[reg]) - if filter_function(instruction): - levels.append(op_stack[reg_ints[ind]] + 1) - else: - levels.append(op_stack[reg_ints[ind]]) - # Assuming here that there is no conditional - # snapshots or barriers ever. - if getattr(instruction.operation, "condition", None): - # Controls operate over all bits of a classical register - # or over a single bit - if isinstance(instruction.operation.condition[0], Clbit): - condition_bits = [instruction.operation.condition[0]] + objects = set(itertools.chain(instruction.qubits, instruction.clbits)) + if (condition := getattr(instruction.operation, "condition", None)) is not None: + objects.update(_builder_utils.condition_resources(condition).clbits) + if isinstance(condition, expr.Expr): + update_from_expr(objects, condition) else: - condition_bits = instruction.operation.condition[0] - for cbit in condition_bits: - idx = bit_indices[cbit] - if idx not in reg_ints: - reg_ints.append(idx) - levels.append(op_stack[idx] + 1) - - max_level = max(levels) - for ind in reg_ints: - op_stack[ind] = max_level - - return max(op_stack) + objects.update(_builder_utils.condition_resources(condition).clbits) + elif isinstance(instruction.operation, SwitchCaseOp): + update_from_expr(objects, expr.lift(instruction.operation.target)) + elif isinstance(instruction.operation, Store): + update_from_expr(objects, instruction.operation.lvalue) + update_from_expr(objects, instruction.operation.rvalue) + + # If we're counting this as adding to depth, do so. If not, it still functions as a + # data synchronisation point between the objects (think "barrier"), so the depths still + # get updated to match the current max over the affected objects. + new_depth = max((obj_depths[obj] for obj in objects), default=0) + if filter_function(instruction): + new_depth += 1 + for obj in objects: + obj_depths[obj] = new_depth + return max(obj_depths.values(), default=0) def width(self) -> int: """Return number of qubits plus clbits in circuit. diff --git a/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml new file mode 100644 index 00000000000..a0744b3dd89 --- /dev/null +++ b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.depth` will now correctly handle operations that + do not have operands, such as :class:`.GlobalPhaseGate`. + - | + :meth:`.QuantumCircuit.depth` will now count the variables and clbits + used in real-time expressions as part of the depth calculation. diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index 481f2fe3ca5..d51dd0c7561 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -17,7 +17,8 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse from qiskit.circuit import Clbit -from qiskit.circuit.library import RXGate, RYGate +from qiskit.circuit.classical import expr, types +from qiskit.circuit.library import RXGate, RYGate, GlobalPhaseGate from qiskit.circuit.exceptions import CircuitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -638,6 +639,58 @@ def test_circuit_depth_first_qubit(self): circ.measure(1, 0) self.assertEqual(circ.depth(lambda x: circ.qubits[0] in x.qubits), 3) + def test_circuit_depth_0_operands(self): + """Test that the depth can be found even with zero-bit operands.""" + qc = QuantumCircuit(2, 2) + qc.append(GlobalPhaseGate(0.0), [], []) + qc.append(GlobalPhaseGate(0.0), [], []) + qc.append(GlobalPhaseGate(0.0), [], []) + self.assertEqual(qc.depth(), 0) + qc.measure([0, 1], [0, 1]) + self.assertEqual(qc.depth(), 1) + + def test_circuit_depth_expr_condition(self): + """Test that circuit depth respects `Expr` conditions in `IfElseOp`.""" + # Note that the "depth" of control-flow operations is not well defined, so the assertions + # here are quite weak. We're mostly aiming to match legacy behaviour of `c_if` for cases + # where there's a single instruction within the conditional. + qc = QuantumCircuit(2, 2) + a = qc.add_input("a", types.Bool()) + with qc.if_test(a): + qc.x(0) + with qc.if_test(expr.logic_and(a, qc.clbits[0])): + qc.x(1) + self.assertEqual(qc.depth(), 2) + qc.measure([0, 1], [0, 1]) + self.assertEqual(qc.depth(), 3) + + def test_circuit_depth_expr_store(self): + """Test that circuit depth respects `Store`.""" + qc = QuantumCircuit(3, 3) + a = qc.add_input("a", types.Bool()) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + # Note that `Store` is a "directive", so doesn't increase the depth by default, but does + # cause qubits 0,1; clbits 0,1 and 'a' to all be depth 3 at this point. + qc.store(a, qc.clbits[0]) + qc.store(a, expr.logic_and(a, qc.clbits[1])) + # ... so this use of 'a' should make it depth 4. + with qc.if_test(a): + qc.x(2) + self.assertEqual(qc.depth(), 4) + + def test_circuit_depth_switch(self): + """Test that circuit depth respects the `target` of `SwitchCaseOp`.""" + qc = QuantumCircuit(QuantumRegister(3, "q"), ClassicalRegister(3, "c")) + a = qc.add_input("a", types.Uint(3)) + + with qc.switch(expr.bit_and(a, qc.cregs[0])) as case: + with case(case.DEFAULT): + qc.x(0) + qc.measure(1, 0) + self.assertEqual(qc.depth(), 2) + def test_circuit_size_empty(self): """Circuit.size should return 0 for an empty circuit.""" size = 4 From 0b1c8bfd18d422a3bdb5ef3c2c4fff977cdbe390 Mon Sep 17 00:00:00 2001 From: shravanpatel30 <78003234+shravanpatel30@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:04:09 -0500 Subject: [PATCH 101/159] [unitaryHACK] Controlling the Insertion of Multi-Qubit Gates in the Generation of Random Circuits #12059 (#12483) * unitaryHACK Controlling the Insertion of Multi-Qubit Gates in the Generation of Random Circuits #12059 * Fixed linting issues * Fixed long lines and unused variable * Added requested changes * Removed unused imports * Added a test * Added the stochastic process comment and edited releasenotes * Update qiskit/circuit/random/utils.py * lint... --------- Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> --- qiskit/circuit/random/utils.py | 123 ++++++++++++++---- ...nded-random-circuits-049b67cce39003f4.yaml | 21 +++ test/python/circuit/test_random_circuit.py | 81 +++++++++++- 3 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index fc497a300cb..f27cbfbfca8 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -21,7 +21,14 @@ def random_circuit( - num_qubits, depth, max_operands=4, measure=False, conditional=False, reset=False, seed=None + num_qubits, + depth, + max_operands=4, + measure=False, + conditional=False, + reset=False, + seed=None, + num_operand_distribution: dict = None, ): """Generate random circuit of arbitrary size and form. @@ -44,6 +51,10 @@ def random_circuit( conditional (bool): if True, insert middle measurements and conditionals reset (bool): if True, insert middle resets seed (int): sets random seed (optional) + num_operand_distribution (dict): a distribution of gates that specifies the ratio + of 1-qubit, 2-qubit, 3-qubit, ..., n-qubit gates in the random circuit. Expect a + deviation from the specified ratios that depends on the size of the requested + random circuit. (optional) Returns: QuantumCircuit: constructed circuit @@ -51,11 +62,38 @@ def random_circuit( Raises: CircuitError: when invalid options given """ + if seed is None: + seed = np.random.randint(0, np.iinfo(np.int32).max) + rng = np.random.default_rng(seed) + + if num_operand_distribution: + if min(num_operand_distribution.keys()) < 1 or max(num_operand_distribution.keys()) > 4: + raise CircuitError("'num_operand_distribution' must have keys between 1 and 4") + for key, prob in num_operand_distribution.items(): + if key > num_qubits and prob != 0.0: + raise CircuitError( + f"'num_operand_distribution' cannot have {key}-qubit gates" + f" for circuit with {num_qubits} qubits" + ) + num_operand_distribution = dict(sorted(num_operand_distribution.items())) + + if not num_operand_distribution and max_operands: + if max_operands < 1 or max_operands > 4: + raise CircuitError("max_operands must be between 1 and 4") + max_operands = max_operands if num_qubits > max_operands else num_qubits + rand_dist = rng.dirichlet( + np.ones(max_operands) + ) # This will create a random distribution that sums to 1 + num_operand_distribution = {i + 1: rand_dist[i] for i in range(max_operands)} + num_operand_distribution = dict(sorted(num_operand_distribution.items())) + + # Here we will use np.isclose() because very rarely there might be floating + # point precision errors + if not np.isclose(sum(num_operand_distribution.values()), 1): + raise CircuitError("The sum of all the values in 'num_operand_distribution' is not 1.") + if num_qubits == 0: return QuantumCircuit() - if max_operands < 1 or max_operands > 4: - raise CircuitError("max_operands must be between 1 and 4") - max_operands = max_operands if num_qubits > max_operands else num_qubits gates_1q = [ # (Gate class, number of qubits, number of parameters) @@ -119,17 +157,26 @@ def random_circuit( (standard_gates.RC3XGate, 4, 0), ] - gates = gates_1q.copy() - if max_operands >= 2: - gates.extend(gates_2q) - if max_operands >= 3: - gates.extend(gates_3q) - if max_operands >= 4: - gates.extend(gates_4q) - gates = np.array( - gates, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] + gates_1q = np.array( + gates_1q, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] ) - gates_1q = np.array(gates_1q, dtype=gates.dtype) + gates_2q = np.array(gates_2q, dtype=gates_1q.dtype) + gates_3q = np.array(gates_3q, dtype=gates_1q.dtype) + gates_4q = np.array(gates_4q, dtype=gates_1q.dtype) + + all_gate_lists = [gates_1q, gates_2q, gates_3q, gates_4q] + + # Here we will create a list 'gates_to_consider' that will have a + # subset of different n-qubit gates and will also create a list for + # ratio (or probability) for each gates + gates_to_consider = [] + distribution = [] + for n_qubits, ratio in num_operand_distribution.items(): + gate_list = all_gate_lists[n_qubits - 1] + gates_to_consider.extend(gate_list) + distribution.extend([ratio / len(gate_list)] * len(gate_list)) + + gates = np.array(gates_to_consider, dtype=gates_1q.dtype) qc = QuantumCircuit(num_qubits) @@ -137,29 +184,60 @@ def random_circuit( cr = ClassicalRegister(num_qubits, "c") qc.add_register(cr) - if seed is None: - seed = np.random.randint(0, np.iinfo(np.int32).max) - rng = np.random.default_rng(seed) - qubits = np.array(qc.qubits, dtype=object, copy=True) + # Counter to keep track of number of different gate types + counter = np.zeros(len(all_gate_lists) + 1, dtype=np.int64) + total_gates = 0 + # Apply arbitrary random operations in layers across all qubits. for layer_number in range(depth): # We generate all the randomness for the layer in one go, to avoid many separate calls to # the randomisation routines, which can be fairly slow. - # This reliably draws too much randomness, but it's less expensive than looping over more # calls to the rng. After, trim it down by finding the point when we've used all the qubits. - gate_specs = rng.choice(gates, size=len(qubits)) + + # Due to the stochastic nature of generating a random circuit, the resulting ratios + # may not precisely match the specified values from `num_operand_distribution`. Expect + # greater deviations from the target ratios in quantum circuits with fewer qubits and + # shallower depths, and smaller deviations in larger and deeper quantum circuits. + # For more information on how the distribution changes with number of qubits and depth + # refer to the pull request #12483 on Qiskit GitHub. + + gate_specs = rng.choice(gates, size=len(qubits), p=distribution) cumulative_qubits = np.cumsum(gate_specs["num_qubits"], dtype=np.int64) + # Efficiently find the point in the list where the total gates would use as many as # possible of, but not more than, the number of qubits in the layer. If there's slack, fill # it with 1q gates. max_index = np.searchsorted(cumulative_qubits, num_qubits, side="right") gate_specs = gate_specs[:max_index] + slack = num_qubits - cumulative_qubits[max_index - 1] - if slack: - gate_specs = np.hstack((gate_specs, rng.choice(gates_1q, size=slack))) + + # Updating the counter for 1-qubit, 2-qubit, 3-qubit and 4-qubit gates + gate_qubits = gate_specs["num_qubits"] + counter += np.bincount(gate_qubits, minlength=len(all_gate_lists) + 1) + + total_gates += len(gate_specs) + + # Slack handling loop, this loop will add gates to fill + # the slack while respecting the 'num_operand_distribution' + while slack > 0: + gate_added_flag = False + + for key, dist in sorted(num_operand_distribution.items(), reverse=True): + if slack >= key and counter[key] / total_gates < dist: + gate_to_add = np.array( + all_gate_lists[key - 1][rng.integers(0, len(all_gate_lists[key - 1]))] + ) + gate_specs = np.hstack((gate_specs, gate_to_add)) + counter[key] += 1 + total_gates += 1 + slack -= key + gate_added_flag = True + if not gate_added_flag: + break # For efficiency in the Python loop, this uses Numpy vectorisation to pre-calculate the # indices into the lists of qubits and parameters for every gate, and then suitably @@ -202,7 +280,6 @@ def random_circuit( ): operation = gate(*parameters[p_start:p_end]) qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) - if measure: qc.measure(qc.qubits, cr) diff --git a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml new file mode 100644 index 00000000000..f4bb585053b --- /dev/null +++ b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml @@ -0,0 +1,21 @@ +--- +features_circuits: + - | + The `random_circuit` function from `qiskit.circuit.random.utils` has a new feature where + users can specify a distribution `num_operand_distribution` (a dict) that specifies the + ratio of 1-qubit, 2-qubit, 3-qubit, and 4-qubit gates in the random circuit. For example, + if `num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}` is passed to the function + then the generated circuit will have approximately 25% of 1-qubit, 2-qubit, 3-qubit, and + 4-qubit gates (The order in which the dictionary is passed does not matter i.e. you can specify + `num_operand_distribution = {3: 0.5, 1: 0.0, 4: 0.3, 2: 0.2}` and the function will still work + as expected). Also it should be noted that the if `num_operand_distribution` is not specified + then `max_operands` will default to 4 and a random circuit with a random gate distribution will + be generated. If both `num_operand_distribution` and `max_operands` are specified at the same + time then `num_operand_distribution` will be used to generate the random circuit. + Example usage:: + + from qiskit.circuit.random import random_circuit + + circ = random_circuit(num_qubits=6, depth=5, num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}) + circ.draw(output='mpl') + diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index deadcd09d69..ebbdfd28d64 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -12,6 +12,7 @@ """Test random circuit generation utility.""" +import numpy as np from qiskit.circuit import QuantumCircuit, ClassicalRegister, Clbit from qiskit.circuit import Measure from qiskit.circuit.random import random_circuit @@ -71,7 +72,7 @@ def test_large_conditional(self): def test_random_mid_circuit_measure_conditional(self): """Test random circuit with mid-circuit measurements for conditionals.""" num_qubits = depth = 2 - circ = random_circuit(num_qubits, depth, conditional=True, seed=4) + circ = random_circuit(num_qubits, depth, conditional=True, seed=16) self.assertEqual(circ.width(), 2 * num_qubits) op_names = [instruction.operation.name for instruction in circ] # Before a condition, there needs to be measurement in all the qubits. @@ -81,3 +82,81 @@ def test_random_mid_circuit_measure_conditional(self): bool(getattr(instruction.operation, "condition", None)) for instruction in circ ] self.assertEqual([False, False, False, True], conditions) + + def test_random_circuit_num_operand_distribution(self): + """Test that num_operand_distribution argument generates gates in correct proportion""" + num_qubits = 50 + depth = 300 + num_op_dist = {2: 0.25, 3: 0.25, 1: 0.25, 4: 0.25} + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=123456789 + ) + total_gates = circ.size() + self.assertEqual(circ.width(), num_qubits) + self.assertEqual(circ.depth(), depth) + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + + def test_random_circuit_2and3_qubit_gates_only(self): + """ + Test that the generated random circuit only has 2 and 3 qubit gates, + while disallowing 1-qubit and 4-qubit gates if + num_operand_distribution = {2: some_prob, 3: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {2: 0.5, 3: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=200) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 2 and 3 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 4-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[4], 0.0) + + def test_random_circuit_3and4_qubit_gates_only(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {3: 0.5, 4: 0.5} + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=11111111 + ) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) + + def test_random_circuit_with_zero_distribution(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {1: 0.0, 2: 0.0, 3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {1: 0.0, 2: 0.0, 3: 0.5, 4: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=12) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) From 2b847b8e9b8ca21e6c0e1ce26acb0686aec3175a Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Mon, 10 Jun 2024 04:39:57 -0400 Subject: [PATCH 102/159] Fix bugs with VF2Layout pass and Qiskit Aer 0.13 (#11585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix bugs with V2FLayout pass and Qiskit Aer 0.13 * Update qiskit/transpiler/passes/layout/vf2_layout.py Co-authored-by: Matthew Treinish * test * Update releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * long lines --------- Co-authored-by: Luciano Bello Co-authored-by: Matthew Treinish Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/transpiler/passes/layout/vf2_layout.py | 14 +++++++++---- qiskit/transpiler/passes/layout/vf2_utils.py | 4 ++-- .../notes/fix-vf2-aer-a7306ce07ea81700.yaml | 4 ++++ test/python/transpiler/test_vf2_layout.py | 20 +++++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 4e3077eb1d4..626f8f2b0fa 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -104,15 +104,21 @@ def __init__( limit on the number of trials will be set. target (Target): A target representing the backend device to run ``VF2Layout`` on. If specified it will supersede a set value for ``properties`` and - ``coupling_map``. + ``coupling_map`` if the :class:`.Target` contains connectivity constraints. If the value + of ``target`` models an ideal backend without any constraints then the value of + ``coupling_map`` + will be used. Raises: TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. """ super().__init__() self.target = target - if target is not None: - self.coupling_map = self.target.build_coupling_map() + if ( + target is not None + and (target_coupling_map := self.target.build_coupling_map()) is not None + ): + self.coupling_map = target_coupling_map else: self.coupling_map = coupling_map self.properties = properties @@ -145,7 +151,7 @@ def run(self, dag): ) # Filter qubits without any supported operations. If they don't support any operations # They're not valid for layout selection - if self.target is not None: + if self.target is not None and self.target.qargs is not None: has_operations = set(itertools.chain.from_iterable(self.target.qargs)) to_remove = set(range(len(cm_nodes))).difference(has_operations) if to_remove: diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 99006017482..c5d420127f8 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -145,7 +145,7 @@ def score_layout( def build_average_error_map(target, properties, coupling_map): """Build an average error map used for scoring layouts pre-basis translation.""" num_qubits = 0 - if target is not None: + if target is not None and target.qargs is not None: num_qubits = target.num_qubits avg_map = ErrorMap(len(target.qargs)) elif coupling_map is not None: @@ -157,7 +157,7 @@ def build_average_error_map(target, properties, coupling_map): # object avg_map = ErrorMap(0) built = False - if target is not None: + if target is not None and target.qargs is not None: for qargs in target.qargs: if qargs is None: continue diff --git a/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml b/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml new file mode 100644 index 00000000000..52ea96d0984 --- /dev/null +++ b/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml @@ -0,0 +1,4 @@ +fixes: + - | + The :class:`.VF2Layout` pass would raise an exception when provided with a :class:`.Target` instance without connectivity constraints. + This would be the case with targets from Aer 0.13. The issue is now fixed. diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index b0957c82468..716e49d3500 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -570,6 +570,26 @@ def test_3_q_gate(self): pass_1.property_set["VF2Layout_stop_reason"], VF2LayoutStopReason.MORE_THAN_2Q ) + def test_target_without_coupling_map(self): + """When a target has no coupling_map but it is provided as argument. + See: https://github.com/Qiskit/qiskit/pull/11585""" + + circuit = QuantumCircuit(3) + circuit.cx(0, 1) + dag = circuit_to_dag(circuit) + + target = Target(num_qubits=3) + target.add_instruction(CXGate()) + + vf2_pass = VF2Layout( + coupling_map=CouplingMap([[0, 2], [1, 2]]), target=target, seed=42, max_trials=1 + ) + vf2_pass.run(dag) + + self.assertEqual( + vf2_pass.property_set["VF2Layout_stop_reason"], VF2LayoutStopReason.SOLUTION_FOUND + ) + class TestMultipleTrials(QiskitTestCase): """Test the passes behavior with >1 trial.""" From f4ca3da35e615e4732d5b561ae45bd83f7f5ac8d Mon Sep 17 00:00:00 2001 From: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:33:23 +0530 Subject: [PATCH 103/159] Fix broken BasisTranslator translation error link (#12533) --- qiskit/transpiler/passes/basis/basis_translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 04bf852e55b..bcac1f5d4fa 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -207,7 +207,7 @@ def run(self, dag): "target basis is not universal or there are additional equivalence rules " "needed in the EquivalenceLibrary being used. For more details on this " "error see: " - "https://docs.quantum.ibm.com/api/qiskit/transpiler_passes." + "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." "BasisTranslator#translation-errors" ) @@ -225,7 +225,7 @@ def run(self, dag): f"basis: {list(target_basis)}. This likely means the target basis is not universal " "or there are additional equivalence rules needed in the EquivalenceLibrary being " "used. For more details on this error see: " - "https://docs.quantum.ibm.com/api/qiskit/transpiler_passes." + "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." "BasisTranslator#translation-errors" ) From b2c3ffd7383f14e71bdf6385213e9f6d5cda4021 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Mon, 10 Jun 2024 06:39:18 -0400 Subject: [PATCH 104/159] Improve public type annotations for `OneQubitEulerDecomposer` (#12530) --- qiskit/synthesis/one_qubit/one_qubit_decompose.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index 5ca44d43d5b..c84db761b7f 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -14,6 +14,7 @@ Decompose a single-qubit unitary via Euler angles. """ from __future__ import annotations +from typing import TYPE_CHECKING import numpy as np from qiskit._accelerate import euler_one_qubit_decomposer @@ -37,6 +38,9 @@ from qiskit.circuit.gate import Gate from qiskit.quantum_info.operators.operator import Operator +if TYPE_CHECKING: + from qiskit.dagcircuit import DAGCircuit + DEFAULT_ATOL = 1e-12 ONE_QUBIT_EULER_BASIS_GATES = { @@ -150,7 +154,7 @@ def __init__(self, basis: str = "U3", use_dag: bool = False): self.basis = basis # sets: self._basis, self._params, self._circuit self.use_dag = use_dag - def build_circuit(self, gates, global_phase): + def build_circuit(self, gates, global_phase) -> QuantumCircuit | DAGCircuit: """Return the circuit or dag object from a list of gates.""" qr = [Qubit()] lookup_gate = False @@ -186,7 +190,7 @@ def __call__( unitary: Operator | Gate | np.ndarray, simplify: bool = True, atol: float = DEFAULT_ATOL, - ) -> QuantumCircuit: + ) -> QuantumCircuit | DAGCircuit: """Decompose single qubit gate into a circuit. Args: From 1956220509e165b00396142236b260c49ee3fdbb Mon Sep 17 00:00:00 2001 From: jpacold Date: Mon, 10 Jun 2024 06:00:21 -0600 Subject: [PATCH 105/159] Move utility functions _inverse_pattern and _get_ordered_swap to Rust (#12327) * Move utility functions _inverse_pattern and _get_ordered_swap to Rust * fix formatting and pylint issues * Changed input type to `PyArrayLike1` * Refactor `permutation.rs`, clean up imports, fix coverage error * fix docstring for `_inverse_pattern` Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * fix docstring for `_get_ordered_swap` Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * remove pymodule nesting * remove explicit `AllowTypeChange` * Move input validation out of `_inverse_pattern` and `_get_ordered_swap` --------- Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/lib.rs | 1 + crates/accelerate/src/permutation.rs | 120 ++++++++++++++++++ crates/pyext/src/lib.rs | 9 +- qiskit/__init__.py | 1 + .../permutation/permutation_utils.py | 36 +----- .../synthesis/test_permutation_synthesis.py | 48 ++++++- 6 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 crates/accelerate/src/permutation.rs diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 0af8ea6a0fc..3924c1de409 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -23,6 +23,7 @@ pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; +pub mod permutation; pub mod results; pub mod sabre; pub mod sampled_exp_val; diff --git a/crates/accelerate/src/permutation.rs b/crates/accelerate/src/permutation.rs new file mode 100644 index 00000000000..31ba433ddd3 --- /dev/null +++ b/crates/accelerate/src/permutation.rs @@ -0,0 +1,120 @@ +// 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 ndarray::{Array1, ArrayView1}; +use numpy::PyArrayLike1; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use std::vec::Vec; + +fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { + let n = pattern.len(); + let mut seen: Vec = vec![false; n]; + + for &x in pattern { + if x < 0 { + return Err(PyValueError::new_err( + "Invalid permutation: input contains a negative number.", + )); + } + + if x as usize >= n { + return Err(PyValueError::new_err(format!( + "Invalid permutation: input has length {} and contains {}.", + n, x + ))); + } + + if seen[x as usize] { + return Err(PyValueError::new_err(format!( + "Invalid permutation: input contains {} more than once.", + x + ))); + } + + seen[x as usize] = true; + } + + Ok(()) +} + +fn invert(pattern: &ArrayView1) -> Array1 { + let mut inverse: Array1 = Array1::zeros(pattern.len()); + pattern.iter().enumerate().for_each(|(ii, &jj)| { + inverse[jj as usize] = ii; + }); + inverse +} + +fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { + let mut permutation: Vec = pattern.iter().map(|&x| x as usize).collect(); + let mut index_map = invert(pattern); + + let n = permutation.len(); + let mut swaps: Vec<(i64, i64)> = Vec::with_capacity(n); + for ii in 0..n { + let val = permutation[ii]; + if val == ii { + continue; + } + let jj = index_map[ii]; + swaps.push((ii as i64, jj as i64)); + (permutation[ii], permutation[jj]) = (permutation[jj], permutation[ii]); + index_map[val] = jj; + index_map[ii] = ii; + } + + swaps[..].reverse(); + swaps +} + +/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. +#[pyfunction] +#[pyo3(signature = (pattern))] +fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + validate_permutation(&view)?; + Ok(py.None()) +} + +/// Finds inverse of a permutation pattern. +#[pyfunction] +#[pyo3(signature = (pattern))] +fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let inverse_i64: Vec = invert(&view).iter().map(|&x| x as i64).collect(); + Ok(inverse_i64.to_object(py)) +} + +/// Sorts the input permutation by iterating through the permutation list +/// and putting each element to its correct position via a SWAP (if it's not +/// at the correct position already). If ``n`` is the length of the input +/// permutation, this requires at most ``n`` SWAPs. +/// +/// More precisely, if the input permutation is a cycle of length ``m``, +/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); +/// if the input permutation consists of several disjoint cycles, then each cycle +/// is essentially treated independently. +#[pyfunction] +#[pyo3(signature = (permutation_in))] +fn _get_ordered_swap(py: Python, permutation_in: PyArrayLike1) -> PyResult { + let view = permutation_in.as_array(); + Ok(get_ordered_swap(&view).to_object(py)) +} + +#[pymodule] +pub fn permutation(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?; + m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?; + m.add_function(wrap_pyfunction!(_get_ordered_swap, m)?)?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index a21b1307a88..b80aad1a7a4 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -17,10 +17,10 @@ use qiskit_accelerate::{ convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, 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, - two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, - vf2_layout::vf2_layout, + pauli_exp_val::pauli_expval, permutation::permutation, results::results, sabre::sabre, + sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, + stochastic_swap::stochastic_swap, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, + utils::utils, vf2_layout::vf2_layout, }; #[pymodule] @@ -36,6 +36,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(nlayout))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(pauli_expval))?; + m.add_wrapped(wrap_pymodule!(permutation))?; m.add_wrapped(wrap_pymodule!(results))?; m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index e4fbc1729e5..27126de6df6 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -82,6 +82,7 @@ sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap sys.modules["qiskit._accelerate.two_qubit_decompose"] = qiskit._accelerate.two_qubit_decompose sys.modules["qiskit._accelerate.vf2_layout"] = qiskit._accelerate.vf2_layout +sys.modules["qiskit._accelerate.permutation"] = qiskit._accelerate.permutation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index 6c6d950dc38..dbd73bfe811 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -12,36 +12,12 @@ """Utility functions for handling permutations.""" - -def _get_ordered_swap(permutation_in): - """Sorts the input permutation by iterating through the permutation list - and putting each element to its correct position via a SWAP (if it's not - at the correct position already). If ``n`` is the length of the input - permutation, this requires at most ``n`` SWAPs. - - More precisely, if the input permutation is a cycle of length ``m``, - then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); - if the input permutation consists of several disjoint cycles, then each cycle - is essentially treated independently. - """ - permutation = list(permutation_in[:]) - swap_list = [] - index_map = _inverse_pattern(permutation_in) - for i, val in enumerate(permutation): - if val != i: - j = index_map[i] - swap_list.append((i, j)) - permutation[i], permutation[j] = permutation[j], permutation[i] - index_map[val] = j - index_map[i] = i - swap_list.reverse() - return swap_list - - -def _inverse_pattern(pattern): - """Finds inverse of a permutation pattern.""" - b_map = {pos: idx for idx, pos in enumerate(pattern)} - return [b_map[pos] for pos in range(len(pattern))] +# pylint: disable=unused-import +from qiskit._accelerate.permutation import ( + _inverse_pattern, + _get_ordered_swap, + _validate_permutation, +) def _pattern_to_cycles(pattern): diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 5c4317ed58a..a879d5251f9 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -25,7 +25,11 @@ synth_permutation_basic, synth_permutation_reverse_lnn_kms, ) -from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap +from qiskit.synthesis.permutation.permutation_utils import ( + _inverse_pattern, + _get_ordered_swap, + _validate_permutation, +) from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -33,9 +37,19 @@ class TestPermutationSynthesis(QiskitTestCase): """Test the permutation synthesis functions.""" + @data(4, 5, 10, 15, 20) + def test_inverse_pattern(self, width): + """Test _inverse_pattern function produces correct index map.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + inverse = _inverse_pattern(pattern) + for ii, jj in enumerate(pattern): + self.assertTrue(inverse[jj] == ii) + @data(4, 5, 10, 15, 20) def test_get_ordered_swap(self, width): - """Test get_ordered_swap function produces correct swap list.""" + """Test _get_ordered_swap function produces correct swap list.""" np.random.seed(1) for _ in range(5): pattern = np.random.permutation(width) @@ -46,6 +60,36 @@ def test_get_ordered_swap(self, width): self.assertTrue(np.array_equal(pattern, output)) self.assertLess(len(swap_list), width) + @data(10, 20) + def test_invalid_permutations(self, width): + """Check that _validate_permutation raises exceptions when the + input is not a permutation.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + + pattern_out_of_range = np.copy(pattern) + pattern_out_of_range[0] = -1 + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_out_of_range) + self.assertIn("input contains a negative number", str(exc.exception)) + + pattern_out_of_range = np.copy(pattern) + pattern_out_of_range[0] = width + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_out_of_range) + self.assertIn( + "input has length {0} and contains {0}".format(width), str(exc.exception) + ) + + pattern_duplicate = np.copy(pattern) + pattern_duplicate[-1] = pattern[0] + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_duplicate) + self.assertIn( + "input contains {} more than once".format(pattern[0]), str(exc.exception) + ) + @data(4, 5, 10, 15, 20) def test_synth_permutation_basic(self, width): """Test synth_permutation_basic function produces the correct From d56a93325acf258552fb54d474eb7466a6bea7f6 Mon Sep 17 00:00:00 2001 From: "S.S" <66886825+EarlMilktea@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:19:42 +0900 Subject: [PATCH 106/159] Fix annotation (#12535) * :bug: Fix annotation * :art: Format by black --- qiskit/providers/providerutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/providers/providerutils.py b/qiskit/providers/providerutils.py index 1e65499d756..36592dc1bc6 100644 --- a/qiskit/providers/providerutils.py +++ b/qiskit/providers/providerutils.py @@ -21,7 +21,9 @@ logger = logging.getLogger(__name__) -def filter_backends(backends: list[Backend], filters: Callable = None, **kwargs) -> list[Backend]: +def filter_backends( + backends: list[Backend], filters: Callable[[Backend], bool] | None = None, **kwargs +) -> list[Backend]: """Return the backends matching the specified filtering. Filter the `backends` list by their `configuration` or `status` From b83efff3b4116406e1875cbaf89f579d7e6e4c53 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Mon, 10 Jun 2024 12:27:28 -0400 Subject: [PATCH 107/159] Moving group of lint rules (#12315) * Moving the arguments-renamed, no-member, no-value-for-parameter, not-context-manager, unexpected-keyword-arg, unnecessary-dunder-call, unnecessary-lambda-assignment, and unspecified-encoding lint rules * updates based on PR and moving not-context-manager back to temp disabled * inline disable no-value-for-parameter lint exception * "unnecessary-dunder-call" wrongly removed --------- Co-authored-by: Luciano Bello --- pyproject.toml | 11 ++++------- qiskit/primitives/backend_estimator.py | 2 +- qiskit/primitives/backend_sampler.py | 2 +- qiskit/providers/options.py | 2 +- qiskit/transpiler/basepasses.py | 2 +- qiskit/transpiler/passmanager.py | 6 +++--- test/python/circuit/test_unitary.py | 2 +- test/python/compiler/test_transpiler.py | 2 +- 8 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e57fa53a7e..fe6036d3abe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -209,6 +209,7 @@ disable = [ "too-many-lines", "too-many-branches", "too-many-locals", "too-many-nested-blocks", "too-many-statements", "too-many-instance-attributes", "too-many-arguments", "too-many-public-methods", "too-few-public-methods", "too-many-ancestors", "unnecessary-pass", # allow for methods with just "pass", for clarity + "unnecessary-dunder-call", # do not want to implement "no-else-return", # relax "elif" after a clause with a return "docstring-first-line-empty", # relax docstring style "import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages @@ -217,17 +218,13 @@ disable = [ # TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale - "arguments-renamed", "consider-using-enumerate", "consider-using-f-string", - "no-member", - "no-value-for-parameter", + "no-member", # for dynamically created members "not-context-manager", "possibly-used-before-assignment", - "unexpected-keyword-arg", - "unnecessary-dunder-call", - "unnecessary-lambda-assignment", - "unspecified-encoding", + "unnecessary-lambda-assignment", # do not want to implement + "unspecified-encoding", # do not want to implement ] enable = [ diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 23556e2efe2..b91ea7068be 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -198,7 +198,7 @@ def _transpile(self): transpiled_circuit = common_circuit.copy() final_index_layout = list(range(common_circuit.num_qubits)) else: - transpiled_circuit = transpile( + transpiled_circuit = transpile( # pylint:disable=unexpected-keyword-arg common_circuit, self.backend, **self.transpile_options.__dict__ ) if transpiled_circuit.layout is not None: diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 94c1c3c88b5..f1399a54893 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -176,7 +176,7 @@ def _transpile(self): start = len(self._transpiled_circuits) self._transpiled_circuits.extend( - transpile( + transpile( # pylint:disable=unexpected-keyword-arg self.preprocessed_circuits[start:], self.backend, **self.transpile_options.__dict__, diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index 659af518a2b..8b2bffc52d8 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -154,7 +154,7 @@ def __copy__(self): The returned option and validator values are shallow copies of the originals. """ - out = self.__new__(type(self)) + out = self.__new__(type(self)) # pylint:disable=no-value-for-parameter out.__setstate__((self._fields.copy(), self.validator.copy())) return out diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index c09ee190e38..396f5cf4934 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -87,7 +87,7 @@ def __eq__(self, other): return hash(self) == hash(other) @abstractmethod - def run(self, dag: DAGCircuit): # pylint: disable=arguments-differ + def run(self, dag: DAGCircuit): # pylint:disable=arguments-renamed """Run a pass on the DAGCircuit. This is implemented by the pass developer. Args: diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 96c0be11b44..eec148e8db2 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -139,7 +139,7 @@ def _finalize_layouts(self, dag): self.property_set["layout"] = t_initial_layout self.property_set["final_layout"] = new_final_layout - def append( + def append( # pylint:disable=arguments-renamed self, passes: Task | list[Task], ) -> None: @@ -153,7 +153,7 @@ def append( """ super().append(tasks=passes) - def replace( + def replace( # pylint:disable=arguments-renamed self, index: int, passes: Task | list[Task], @@ -167,7 +167,7 @@ def replace( super().replace(index, tasks=passes) # pylint: disable=arguments-differ - def run( + def run( # pylint:disable=arguments-renamed self, circuits: _CircuitsT, output_name: str | None = None, diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 23aec666cbd..c5c9344ad7e 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -178,7 +178,7 @@ def test_qobj_with_unitary_matrix(self): class NumpyEncoder(json.JSONEncoder): """Class for encoding json str with complex and numpy arrays.""" - def default(self, obj): + def default(self, obj): # pylint:disable=arguments-renamed if isinstance(obj, numpy.ndarray): return obj.tolist() if isinstance(obj, complex): diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 30ba83440c9..6058f922e17 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2864,7 +2864,7 @@ def max_circuits(self): def _default_options(cls): return Options(shots=1024) - def run(self, circuit, **kwargs): + def run(self, circuit, **kwargs): # pylint:disable=arguments-renamed raise NotImplementedError self.backend = FakeMultiChip() From 03d107e1f68f2a47ffdcf5599f906120e4efba63 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Mon, 10 Jun 2024 16:39:48 -0400 Subject: [PATCH 108/159] Removing consider-using-enumerate from lint exclusions and updates (#12286) * removing consider-using-enumerate from lint exclusions and updates * updating for lint removal of consider-using-enumerate * adding disable lint comment for consider-using-enumerate * updating loops for getting the original, ordered list of qubits in passmanager.py * reverting update and adding disable comment --- pyproject.toml | 1 - .../piecewise_polynomial_pauli_rotations.py | 4 +- .../stabilizer/stabilizer_circuit.py | 12 +++--- .../optimization/inverse_cancellation.py | 8 ++-- .../template_matching/forward_match.py | 4 +- qiskit/transpiler/passmanager.py | 1 + qiskit/visualization/bloch.py | 37 ++++++++----------- test/python/synthesis/test_synthesis.py | 6 +-- 8 files changed, 34 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fe6036d3abe..c13486c21bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,7 +218,6 @@ disable = [ # TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale - "consider-using-enumerate", "consider-using-f-string", "no-member", # for dynamically created members "not-context-manager", diff --git a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py index f604e16f469..7e79ed04da1 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py @@ -218,8 +218,8 @@ def evaluate(self, x: float) -> float: """ y = 0 - for i in range(0, len(self.breakpoints)): - y = y + (x >= self.breakpoints[i]) * (np.poly1d(self.mapped_coeffs[i][::-1])(x)) + for i, breakpt in enumerate(self.breakpoints): + y = y + (x >= breakpt) * (np.poly1d(self.mapped_coeffs[i][::-1])(x)) return y diff --git a/qiskit/synthesis/stabilizer/stabilizer_circuit.py b/qiskit/synthesis/stabilizer/stabilizer_circuit.py index 3882676be7d..4a5d53a7322 100644 --- a/qiskit/synthesis/stabilizer/stabilizer_circuit.py +++ b/qiskit/synthesis/stabilizer/stabilizer_circuit.py @@ -68,8 +68,8 @@ def synth_circuit_from_stabilizers( circuit = QuantumCircuit(num_qubits) used = 0 - for i in range(len(stabilizer_list)): - curr_stab = stabilizer_list[i].evolve(Clifford(circuit), frame="s") + for i, stabilizer in enumerate(stabilizer_list): + curr_stab = stabilizer.evolve(Clifford(circuit), frame="s") # Find pivot. pivot = used @@ -81,17 +81,17 @@ def synth_circuit_from_stabilizers( if pivot == num_qubits: if curr_stab.x.any(): raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) anti-commutes with some of " + f"Stabilizer {i} ({stabilizer}) anti-commutes with some of " "the previous stabilizers." ) if curr_stab.phase == 2: raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) contradicts " + f"Stabilizer {i} ({stabilizer}) contradicts " "some of the previous stabilizers." ) if curr_stab.z.any() and not allow_redundant: raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) is a product of the others " + f"Stabilizer {i} ({stabilizer}) is a product of the others " "and allow_redundant is False. Add allow_redundant=True " "to the function call if you want to allow redundant stabilizers." ) @@ -133,7 +133,7 @@ def synth_circuit_from_stabilizers( circuit.swap(pivot, used) # fix sign - curr_stab = stabilizer_list[i].evolve(Clifford(circuit), frame="s") + curr_stab = stabilizer.evolve(Clifford(circuit), frame="s") if curr_stab.phase == 2: circuit.x(used) used += 1 diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index c814f50d4a1..958f53ef057 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -112,15 +112,15 @@ def _run_on_self_inverse(self, dag: DAGCircuit): partitions = [] chunk = [] max_index = len(gate_cancel_run) - 1 - for i in range(len(gate_cancel_run)): - if gate_cancel_run[i].op == gate: - chunk.append(gate_cancel_run[i]) + for i, cancel_gate in enumerate(gate_cancel_run): + if cancel_gate.op == gate: + chunk.append(cancel_gate) else: if chunk: partitions.append(chunk) chunk = [] continue - if i == max_index or gate_cancel_run[i].qargs != gate_cancel_run[i + 1].qargs: + if i == max_index or cancel_gate.qargs != gate_cancel_run[i + 1].qargs: partitions.append(chunk) chunk = [] # Remove an even number of gates from each chunk diff --git a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py index 627db502d33..d8dd5bb2b9a 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py @@ -138,8 +138,8 @@ def _find_forward_candidates(self, node_id_t): """ matches = [] - for i in range(0, len(self.match)): - matches.append(self.match[i][0]) + for match in self.match: + matches.append(match[0]) pred = matches.copy() if len(pred) > 1: diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index eec148e8db2..f590a1510bd 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -121,6 +121,7 @@ def _finalize_layouts(self, dag): # Ordered list of original qubits original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()} original_qubits = [] + # pylint: disable-next=consider-using-enumerate for i in range(len(original_qubits_reverse)): original_qubits.append(original_qubits_reverse[i]) diff --git a/qiskit/visualization/bloch.py b/qiskit/visualization/bloch.py index 513d2ddff85..2855c6ba965 100644 --- a/qiskit/visualization/bloch.py +++ b/qiskit/visualization/bloch.py @@ -587,11 +587,11 @@ def plot_axes_labels(self): def plot_vectors(self): """Plot vector""" # -X and Y data are switched for plotting purposes - for k in range(len(self.vectors)): + for k, vector in enumerate(self.vectors): - xs3d = self.vectors[k][1] * np.array([0, 1]) - ys3d = -self.vectors[k][0] * np.array([0, 1]) - zs3d = self.vectors[k][2] * np.array([0, 1]) + xs3d = vector[1] * np.array([0, 1]) + ys3d = -vector[0] * np.array([0, 1]) + zs3d = vector[2] * np.array([0, 1]) color = self.vector_color[np.mod(k, len(self.vector_color))] @@ -617,15 +617,10 @@ def plot_vectors(self): def plot_points(self): """Plot points""" # -X and Y data are switched for plotting purposes - for k in range(len(self.points)): - num = len(self.points[k][0]) + for k, point in enumerate(self.points): + num = len(point[0]) dist = [ - np.sqrt( - self.points[k][0][j] ** 2 - + self.points[k][1][j] ** 2 - + self.points[k][2][j] ** 2 - ) - for j in range(num) + np.sqrt(point[0][j] ** 2 + point[1][j] ** 2 + point[2][j] ** 2) for j in range(num) ] if any(abs(dist - dist[0]) / dist[0] > 1e-12): # combine arrays so that they can be sorted together @@ -637,9 +632,9 @@ def plot_points(self): indperm = np.arange(num) if self.point_style[k] == "s": self.axes.scatter( - np.real(self.points[k][1][indperm]), - -np.real(self.points[k][0][indperm]), - np.real(self.points[k][2][indperm]), + np.real(point[1][indperm]), + -np.real(point[0][indperm]), + np.real(point[2][indperm]), s=self.point_size[np.mod(k, len(self.point_size))], alpha=1, edgecolor=None, @@ -656,9 +651,9 @@ def plot_points(self): marker = self.point_marker[np.mod(k, len(self.point_marker))] pnt_size = self.point_size[np.mod(k, len(self.point_size))] self.axes.scatter( - np.real(self.points[k][1][indperm]), - -np.real(self.points[k][0][indperm]), - np.real(self.points[k][2][indperm]), + np.real(point[1][indperm]), + -np.real(point[0][indperm]), + np.real(point[2][indperm]), s=pnt_size, alpha=1, edgecolor=None, @@ -670,9 +665,9 @@ def plot_points(self): elif self.point_style[k] == "l": color = self.point_color[np.mod(k, len(self.point_color))] self.axes.plot( - np.real(self.points[k][1]), - -np.real(self.points[k][0]), - np.real(self.points[k][2]), + np.real(point[1]), + -np.real(point[0]), + np.real(point[2]), alpha=0.75, zdir="z", color=color, diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index cb918b29146..8d953ff1b83 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -138,13 +138,13 @@ def assertDebugOnly(self): # FIXME: when at python 3.10+ replace with assertNoL """Context manager, asserts log is emitted at level DEBUG but no higher""" with self.assertLogs("qiskit.synthesis", "DEBUG") as ctx: yield - for i in range(len(ctx.records)): + for i, record in enumerate(ctx.records): self.assertLessEqual( - ctx.records[i].levelno, + record.levelno, logging.DEBUG, msg=f"Unexpected logging entry: {ctx.output[i]}", ) - self.assertIn("Requested fidelity:", ctx.records[i].getMessage()) + self.assertIn("Requested fidelity:", record.getMessage()) def assertRoundTrip(self, weyl1: TwoQubitWeylDecomposition): """Fail if eval(repr(weyl1)) not equal to weyl1""" From bc685d3002915327f3070ef8914c6c1484084b57 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Tue, 11 Jun 2024 11:58:34 -0400 Subject: [PATCH 109/159] Use hash of numeric value for bound parameter expressions (#12488) * Use hash of numeric value for bound parameter expressions If a `ParameterExpression` has no unbound parameters, the underlying bound value can be hashed instead of the tuple that accounts for the symbolic expression. Doing this allows for the `ParameterExpression` to match the hash for the numeric value it compares equal to. Closes #12487 * Add release note --- qiskit/circuit/parameterexpression.py | 3 +++ .../notes/parameterexpression-hash-d2593ab1715aa42c.yaml | 8 ++++++++ test/python/circuit/test_parameters.py | 1 + 3 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 2b81ddd769f..f881e09333d 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -442,6 +442,9 @@ def __int__(self): raise TypeError("could not cast expression to int") from exc def __hash__(self): + if not self._parameter_symbols: + # For fully bound expressions, fall back to the underlying value + return hash(self.numeric()) return hash((self._parameter_keys, self._symbol_expr)) def __copy__(self): diff --git a/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml new file mode 100644 index 00000000000..075de45b3b2 --- /dev/null +++ b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + :class:`.ParameterExpression` was updated so that fully bound instances + that compare equal to instances of Python's built-in numeric types (like + ``float`` and ``int``) also have hash values that match those of the other + instances. This change ensures that these types can be used interchangeably + as dictionary keys. See `#12488 `__. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index ed82f33eac9..c9380fd0768 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1425,6 +1425,7 @@ def test_compare_to_value_when_bound(self): x = Parameter("x") bound_expr = x.bind({x: 2.3}) self.assertEqual(bound_expr, 2.3) + self.assertEqual(hash(bound_expr), hash(2.3)) def test_abs_function_when_bound(self): """Verify expression can be used with From b933f6d377e7e7535e0fa4ee202ce97d6461b6b1 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 12 Jun 2024 11:14:48 +0200 Subject: [PATCH 110/159] Add option to user config to control `idle_wires` in circuit drawer (#12462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add option to user config to control idle_wires in circuit drawer Co-Authored-By: diemilio * docs * 11339 * Update qiskit/visualization/circuit/circuit_visualization.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --------- Co-authored-by: diemilio Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/circuit/quantumcircuit.py | 28 +++++----- qiskit/user_config.py | 14 +++++ .../circuit/circuit_visualization.py | 12 +++- .../workaroud_12361-994d0ac2d2a6ed41.yaml | 14 +++++ test/python/test_user_config.py | 29 ++++++++++ .../transpiler/test_basis_translator.py | 9 +-- .../visualization/test_circuit_text_drawer.py | 55 ++++++++++++++++++- 7 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ea4361fd825..606d0e04373 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1757,14 +1757,14 @@ def compose( this can be anything that :obj:`.append` will accept. qubits (list[Qubit|int]): qubits of self to compose onto. clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed. This is not possible within + front (bool): If ``True``, front composition will be performed. This is not possible within control-flow builder context managers. - inplace (bool): If True, modify the object. Otherwise return composed circuit. + inplace (bool): If ``True``, modify the object. Otherwise, return composed circuit. copy (bool): If ``True`` (the default), then the input is treated as shared, and any contained instructions will be copied, if they might need to be mutated in the future. You can set this to ``False`` if the input should be considered owned by the base circuit, in order to avoid unnecessary copies; in this case, it is not - valid to use ``other`` afterwards, and some instructions may have been mutated in + valid to use ``other`` afterward, and some instructions may have been mutated in place. var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` nodes in ``other`` as they are inlined into ``self``. This can be used to avoid naming conflicts. @@ -2068,7 +2068,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu Args: other (QuantumCircuit): The other circuit to tensor this circuit with. - inplace (bool): If True, modify the object. Otherwise return composed circuit. + inplace (bool): If ``True``, modify the object. Otherwise return composed circuit. Examples: @@ -2084,7 +2084,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu tensored.draw('mpl') Returns: - QuantumCircuit: The tensored circuit (returns None if inplace==True). + QuantumCircuit: The tensored circuit (returns ``None`` if ``inplace=True``). """ num_qubits = self.num_qubits + other.num_qubits num_clbits = self.num_clbits + other.num_clbits @@ -3126,7 +3126,7 @@ def draw( reverse_bits: bool | None = None, justify: str | None = None, vertical_compression: str | None = "medium", - idle_wires: bool = True, + idle_wires: bool | None = None, with_layout: bool = True, fold: int | None = None, # The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be @@ -3157,7 +3157,7 @@ def draw( Args: output: Select the output method to use for drawing the circuit. Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``. - By default the `text` drawer is used unless the user config file + By default, the ``text`` drawer is used unless the user config file (usually ``~/.qiskit/settings.conf``) has an alternative backend set as the default. For example, ``circuit_drawer = latex``. If the output kwarg is set, that backend will always be used over the default in @@ -3203,7 +3203,9 @@ def draw( will take less vertical room. Default is ``medium``. Only used by the ``text`` output, will be silently ignored otherwise. idle_wires: Include idle wires (wires with no circuit elements) - in output visualization. Default is ``True``. + in output visualization. Default is ``True`` unless the + user config file (usually ``~/.qiskit/settings.conf``) has an + alternative value set. For example, ``circuit_idle_wires = False``. with_layout: Include layout information, with labels on the physical layout. Default is ``True``. fold: Sets pagination. It can be disabled using -1. In ``text``, @@ -3292,7 +3294,7 @@ def size( Args: filter_function (callable): a function to filter out some instructions. Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)). - By default filters out "directives", such as barrier or snapshot. + By default, filters out "directives", such as barrier or snapshot. Returns: int: Total number of gate operations. @@ -3314,7 +3316,7 @@ def depth( filter_function: A function to decide which instructions count to increase depth. Should take as a single positional input a :class:`CircuitInstruction`. Instructions for which the function returns ``False`` are ignored in the - computation of the circuit depth. By default filters out "directives", such as + computation of the circuit depth. By default, filters out "directives", such as :class:`.Barrier`. Returns: @@ -3445,7 +3447,7 @@ def num_connected_components(self, unitary_only: bool = False) -> int: bits = self.qubits if unitary_only else (self.qubits + self.clbits) bit_indices: dict[Qubit | Clbit, int] = {bit: idx for idx, bit in enumerate(bits)} - # Start with each qubit or cbit being its own subgraph. + # Start with each qubit or clbit being its own subgraph. sub_graphs = [[bit] for bit in range(len(bit_indices))] num_sub_graphs = len(sub_graphs) @@ -3816,7 +3818,7 @@ def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: inplace (bool): All measurements inplace or return new circuit. Returns: - QuantumCircuit: Returns circuit with measurements when `inplace = False`. + QuantumCircuit: Returns circuit with measurements when ``inplace = False``. """ from qiskit.converters.circuit_to_dag import circuit_to_dag @@ -5704,7 +5706,7 @@ class to prepare the qubits in a specified state. * Statevector or vector of complex amplitudes to initialize to. * Labels of basis states of the Pauli eigenstates Z, X, Y. See :meth:`.Statevector.from_label`. Notice the order of the labels is reversed with - respect to the qubit index to be applied to. Example label '01' initializes the + respect to the qubit index to be applied to. Example label ``'01'`` initializes the qubit zero to :math:`|1\rangle` and the qubit one to :math:`|0\rangle`. * An integer that is used as a bitmap indicating which qubits to initialize to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 666bf53d962..73a68eb6bfd 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -31,6 +31,7 @@ class UserConfig: circuit_mpl_style = default circuit_mpl_style_path = ~/.qiskit: circuit_reverse_bits = True + circuit_idle_wires = False transpile_optimization_level = 1 parallel = False num_processes = 4 @@ -130,6 +131,18 @@ def read_config_file(self): if circuit_reverse_bits is not None: self.settings["circuit_reverse_bits"] = circuit_reverse_bits + # Parse circuit_idle_wires + try: + circuit_idle_wires = self.config_parser.getboolean( + "default", "circuit_idle_wires", fallback=None + ) + except ValueError as err: + raise exceptions.QiskitUserConfigError( + f"Value assigned to circuit_idle_wires is not valid. {str(err)}" + ) + if circuit_idle_wires is not None: + self.settings["circuit_idle_wires"] = circuit_idle_wires + # Parse transpile_optimization_level transpile_optimization_level = self.config_parser.getint( "default", "transpile_optimization_level", fallback=-1 @@ -191,6 +204,7 @@ def set_config(key, value, section=None, file_path=None): "circuit_mpl_style", "circuit_mpl_style_path", "circuit_reverse_bits", + "circuit_idle_wires", "transpile_optimization_level", "parallel", "num_processes", diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index bea6021c23a..a1672dc1676 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -63,7 +63,7 @@ def circuit_drawer( reverse_bits: bool | None = None, justify: str | None = None, vertical_compression: str | None = "medium", - idle_wires: bool = True, + idle_wires: bool | None = None, with_layout: bool = True, fold: int | None = None, # The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be @@ -115,7 +115,7 @@ def circuit_drawer( output: Select the output method to use for drawing the circuit. Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``. - By default the `text` drawer is used unless the user config file + By default, the ``text`` drawer is used unless the user config file (usually ``~/.qiskit/settings.conf``) has an alternative backend set as the default. For example, ``circuit_drawer = latex``. If the output kwarg is set, that backend will always be used over the default in @@ -141,7 +141,9 @@ def circuit_drawer( will take less vertical room. Default is ``medium``. Only used by the ``text`` output, will be silently ignored otherwise. idle_wires: Include idle wires (wires with no circuit elements) - in output visualization. Default is ``True``. + in output visualization. Default is ``True`` unless the + user config file (usually ``~/.qiskit/settings.conf``) has an + alternative value set. For example, ``circuit_idle_wires = False``. with_layout: Include layout information, with labels on the physical layout. Default is ``True``. fold: Sets pagination. It can be disabled using -1. In ``text``, @@ -200,6 +202,7 @@ def circuit_drawer( # Get default from config file else use text default_output = "text" default_reverse_bits = False + default_idle_wires = config.get("circuit_idle_wires", True) if config: default_output = config.get("circuit_drawer", "text") if default_output == "auto": @@ -215,6 +218,9 @@ def circuit_drawer( if reverse_bits is None: reverse_bits = default_reverse_bits + if idle_wires is None: + idle_wires = default_idle_wires + if wire_order is not None and reverse_bits: raise VisualizationError( "The wire_order option cannot be set when the reverse_bits option is True." diff --git a/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml b/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml new file mode 100644 index 00000000000..9c19be117ed --- /dev/null +++ b/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml @@ -0,0 +1,14 @@ +--- +features_visualization: + - | + The user configuration file has a new option ``circuit_idle_wires``, which takes a Boolean + value. This allows users to set their preferred default behavior of the ``idle_wires`` option + of the circuit drawers :meth:`.QuantumCircuit.draw` and :func:`.circuit_drawer`. For example, + adding a section to ``~/.qiskit/settings.conf`` with: + + .. code-block:: text + + [default] + circuit_idle_wires = false + + will change the default to display the bits in reverse order. diff --git a/test/python/test_user_config.py b/test/python/test_user_config.py index e3e01213463..ecc4ffaaa96 100644 --- a/test/python/test_user_config.py +++ b/test/python/test_user_config.py @@ -94,6 +94,31 @@ def test_circuit_reverse_bits_valid(self): config.read_config_file() self.assertEqual({"circuit_reverse_bits": False}, config.settings) + def test_invalid_circuit_idle_wires(self): + test_config = """ + [default] + circuit_idle_wires = Neither + """ + self.addCleanup(os.remove, self.file_path) + with open(self.file_path, "w") as file: + file.write(test_config) + file.flush() + config = user_config.UserConfig(self.file_path) + self.assertRaises(exceptions.QiskitUserConfigError, config.read_config_file) + + def test_circuit_idle_wires_valid(self): + test_config = """ + [default] + circuit_idle_wires = true + """ + self.addCleanup(os.remove, self.file_path) + with open(self.file_path, "w") as file: + file.write(test_config) + file.flush() + config = user_config.UserConfig(self.file_path) + config.read_config_file() + self.assertEqual({"circuit_idle_wires": True}, config.settings) + def test_optimization_level_valid(self): test_config = """ [default] @@ -152,6 +177,7 @@ def test_all_options_valid(self): circuit_mpl_style = default circuit_mpl_style_path = ~:~/.qiskit circuit_reverse_bits = false + circuit_idle_wires = true transpile_optimization_level = 3 suppress_packaging_warnings = true parallel = false @@ -170,6 +196,7 @@ def test_all_options_valid(self): "circuit_mpl_style": "default", "circuit_mpl_style_path": ["~", "~/.qiskit"], "circuit_reverse_bits": False, + "circuit_idle_wires": True, "transpile_optimization_level": 3, "num_processes": 15, "parallel_enabled": False, @@ -184,6 +211,7 @@ def test_set_config_all_options_valid(self): user_config.set_config("circuit_mpl_style", "default", file_path=self.file_path) user_config.set_config("circuit_mpl_style_path", "~:~/.qiskit", file_path=self.file_path) user_config.set_config("circuit_reverse_bits", "false", file_path=self.file_path) + user_config.set_config("circuit_idle_wires", "true", file_path=self.file_path) user_config.set_config("transpile_optimization_level", "3", file_path=self.file_path) user_config.set_config("parallel", "false", file_path=self.file_path) user_config.set_config("num_processes", "15", file_path=self.file_path) @@ -198,6 +226,7 @@ def test_set_config_all_options_valid(self): "circuit_mpl_style": "default", "circuit_mpl_style_path": ["~", "~/.qiskit"], "circuit_reverse_bits": False, + "circuit_idle_wires": True, "transpile_optimization_level": 3, "num_processes": 15, "parallel_enabled": False, diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 24e5e68ba98..fc933cd8f66 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -1150,15 +1150,16 @@ def setUp(self): self.target.add_instruction(CXGate(), cx_props) def test_2q_with_non_global_1q(self): - """Test translation works with a 2q gate on an non-global 1q basis.""" + """Test translation works with a 2q gate on a non-global 1q basis.""" qc = QuantumCircuit(2) qc.cz(0, 1) bt_pass = BasisTranslator(std_eqlib, target_basis=None, target=self.target) output = bt_pass(qc) - # We need a second run of BasisTranslator to correct gates outside of - # the target basis. This is a known isssue, see: - # https://docs.quantum.ibm.com/api/qiskit/release-notes/0.33#known-issues + # We need a second run of BasisTranslator to correct gates outside + # the target basis. This is a known issue, see: + # https://github.com/Qiskit/qiskit/issues/11339 + # TODO: remove the second bt_pass call once fixed. output = bt_pass(output) expected = QuantumCircuit(2) expected.rz(pi, 1) diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 5f72a7d1bbb..3f018c08510 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -495,6 +495,58 @@ def test_text_reverse_bits_read_from_config(self): test_reverse = str(circuit_drawer(circuit, output="text")) self.assertEqual(test_reverse, expected_reverse) + def test_text_idle_wires_read_from_config(self): + """Swap drawing with idle_wires set in the configuration file.""" + expected_with = "\n".join( + [ + " ┌───┐", + "q1_0: ┤ H ├", + " └───┘", + "q1_1: ─────", + " ┌───┐", + "q2_0: ┤ H ├", + " └───┘", + "q2_1: ─────", + " ", + ] + ) + expected_without = "\n".join( + [ + " ┌───┐", + "q1_0: ┤ H ├", + " ├───┤", + "q2_0: ┤ H ├", + " └───┘", + ] + ) + qr1 = QuantumRegister(2, "q1") + qr2 = QuantumRegister(2, "q2") + circuit = QuantumCircuit(qr1, qr2) + circuit.h(qr1[0]) + circuit.h(qr2[0]) + + self.assertEqual( + str( + circuit_drawer( + circuit, + output="text", + ) + ), + expected_with, + ) + + config_content = """ + [default] + circuit_idle_wires = false + """ + with tempfile.TemporaryDirectory() as dir_path: + file_path = pathlib.Path(dir_path) / "qiskit.conf" + with open(file_path, "w") as fptr: + fptr.write(config_content) + with unittest.mock.patch.dict(os.environ, {"QISKIT_SETTINGS": str(file_path)}): + test_without = str(circuit_drawer(circuit, output="text")) + self.assertEqual(test_without, expected_without) + def test_text_cswap(self): """CSwap drawing.""" expected = "\n".join( @@ -514,6 +566,7 @@ def test_text_cswap(self): circuit.cswap(qr[0], qr[1], qr[2]) circuit.cswap(qr[1], qr[0], qr[2]) circuit.cswap(qr[2], qr[1], qr[0]) + self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) def test_text_cswap_reverse_bits(self): @@ -4223,7 +4276,6 @@ def test_text_4q_2c(self): cr6 = ClassicalRegister(6, "c") circuit = QuantumCircuit(qr6, cr6) circuit.append(inst, qr6[1:5], cr6[1:3]) - self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) def test_text_2q_1c(self): @@ -5668,7 +5720,6 @@ def test_registerless_one_bit(self): qry = QuantumRegister(1, "qry") crx = ClassicalRegister(2, "crx") circuit = QuantumCircuit(qrx, [Qubit(), Qubit()], qry, [Clbit(), Clbit()], crx) - self.assertEqual(circuit.draw(output="text", cregbundle=True).single_string(), expected) From 287e86ce8a6ca3412747c31a5f76fe61d85952f6 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Wed, 12 Jun 2024 05:32:36 -0400 Subject: [PATCH 111/159] Use relative import for `_accelerate` (#12546) * Use relative import for `_accelerate` * Fix black * Disable wrong-import-order in `__init__.py` --- qiskit/__init__.py | 46 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 27126de6df6..fce54433347 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,wrong-import-order """Main Qiskit public functionality.""" @@ -52,37 +52,35 @@ ) -import qiskit._accelerate +from . import _accelerate import qiskit._numpy_compat # Globally define compiled submodules. The normal import mechanism will not find compiled submodules # in _accelerate because it relies on file paths, but PyO3 generates only one shared library file. # We manually define them on import so people can directly import qiskit._accelerate.* submodules # and not have to rely on attribute access. No action needed for top-level extension packages. -sys.modules["qiskit._accelerate.circuit"] = qiskit._accelerate.circuit -sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = ( - qiskit._accelerate.convert_2q_block_matrix -) -sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout -sys.modules["qiskit._accelerate.error_map"] = qiskit._accelerate.error_map -sys.modules["qiskit._accelerate.isometry"] = qiskit._accelerate.isometry -sys.modules["qiskit._accelerate.uc_gate"] = qiskit._accelerate.uc_gate +sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit +sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix +sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout +sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map +sys.modules["qiskit._accelerate.isometry"] = _accelerate.isometry +sys.modules["qiskit._accelerate.uc_gate"] = _accelerate.uc_gate sys.modules["qiskit._accelerate.euler_one_qubit_decomposer"] = ( - qiskit._accelerate.euler_one_qubit_decomposer + _accelerate.euler_one_qubit_decomposer ) -sys.modules["qiskit._accelerate.nlayout"] = qiskit._accelerate.nlayout -sys.modules["qiskit._accelerate.optimize_1q_gates"] = qiskit._accelerate.optimize_1q_gates -sys.modules["qiskit._accelerate.pauli_expval"] = qiskit._accelerate.pauli_expval -sys.modules["qiskit._accelerate.qasm2"] = qiskit._accelerate.qasm2 -sys.modules["qiskit._accelerate.qasm3"] = qiskit._accelerate.qasm3 -sys.modules["qiskit._accelerate.results"] = qiskit._accelerate.results -sys.modules["qiskit._accelerate.sabre"] = qiskit._accelerate.sabre -sys.modules["qiskit._accelerate.sampled_exp_val"] = qiskit._accelerate.sampled_exp_val -sys.modules["qiskit._accelerate.sparse_pauli_op"] = qiskit._accelerate.sparse_pauli_op -sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap -sys.modules["qiskit._accelerate.two_qubit_decompose"] = qiskit._accelerate.two_qubit_decompose -sys.modules["qiskit._accelerate.vf2_layout"] = qiskit._accelerate.vf2_layout -sys.modules["qiskit._accelerate.permutation"] = qiskit._accelerate.permutation +sys.modules["qiskit._accelerate.nlayout"] = _accelerate.nlayout +sys.modules["qiskit._accelerate.optimize_1q_gates"] = _accelerate.optimize_1q_gates +sys.modules["qiskit._accelerate.pauli_expval"] = _accelerate.pauli_expval +sys.modules["qiskit._accelerate.qasm2"] = _accelerate.qasm2 +sys.modules["qiskit._accelerate.qasm3"] = _accelerate.qasm3 +sys.modules["qiskit._accelerate.results"] = _accelerate.results +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.stochastic_swap"] = _accelerate.stochastic_swap +sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose +sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout +sys.modules["qiskit._accelerate.permutation"] = _accelerate.permutation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError From 8d2144cec32d5f20e03a8c9af1999a0b7b7a1645 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 12 Jun 2024 15:02:12 +0200 Subject: [PATCH 112/159] small doc improvements (#12553) * small doc improvements * Update qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/dagcircuit/dagcircuit.py | 2 +- qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py | 2 +- qiskit/transpiler/preset_passmanagers/__init__.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 831851bee36..96562a37b15 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -544,7 +544,7 @@ def remove_qubits(self, *qubits): def remove_qregs(self, *qregs): """ - Remove classical registers from the circuit, leaving underlying bits + Remove quantum registers from the circuit, leaving underlying bits in place. Raises: diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 440a0319c33..f8d25a6e8da 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -54,7 +54,7 @@ class SparsePauliOp(LinearOp): :class:`~qiskit.quantum_info.Operator` in terms of N-qubit :class:`~qiskit.quantum_info.PauliList` and complex coefficients. - It can be used for performing operator arithmetic for hundred of qubits + It can be used for performing operator arithmetic for hundreds of qubits if the number of non-zero Pauli basis terms is sufficiently small. The Pauli basis components are stored as a diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 37b284a4680..f2f011e486c 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -109,12 +109,12 @@ def generate_preset_pass_manager( This function is used to quickly generate a preset pass manager. Preset pass managers are the default pass managers used by the :func:`~.transpile` function. This function provides a convenient and simple method to construct - a standalone :class:`~.PassManager` object that mirrors what the transpile + a standalone :class:`~.PassManager` object that mirrors what the :func:`~.transpile` function internally builds and uses. The target constraints for the pass manager construction can be specified through a :class:`.Target` - instance, a `.BackendV1` or `.BackendV2` instance, or via loose constraints (``basis_gates``, - ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + instance, a :class:`.BackendV1` or :class:`.BackendV2` instance, or via loose constraints + (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, ``dt`` or ``timing_constraints``). The order of priorities for target constraints works as follows: if a ``target`` input is provided, it will take priority over any ``backend`` input or loose constraints. From 8a1bcc2d5236ac090cd9bcbd7cdf5ce8efbcb18c Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Thu, 13 Jun 2024 04:18:43 -0400 Subject: [PATCH 113/159] Do not retain and expose old elements of `ParameterVector` (#12561) * Do not retain and expose old elements of `ParameterVector` This fixes #12541 according to https://github.com/Qiskit/qiskit/pull/12545#pullrequestreview-2114202382. * Fix test --- qiskit/circuit/parametervector.py | 38 +++++++++++++++----------- qiskit/qpy/binary_io/value.py | 2 +- test/python/circuit/test_parameters.py | 6 ++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py index abc8a6f60ef..151e3e7fea7 100644 --- a/qiskit/circuit/parametervector.py +++ b/qiskit/circuit/parametervector.py @@ -50,11 +50,10 @@ def __setstate__(self, state): class ParameterVector: """ParameterVector class to quickly generate lists of parameters.""" - __slots__ = ("_name", "_params", "_size", "_root_uuid") + __slots__ = ("_name", "_params", "_root_uuid") def __init__(self, name, length=0): self._name = name - self._size = length self._root_uuid = uuid4() root_uuid_int = self._root_uuid.int self._params = [ @@ -76,32 +75,38 @@ def index(self, value): return self._params.index(value) def __getitem__(self, key): - if isinstance(key, slice): - start, stop, step = key.indices(self._size) - return self.params[start:stop:step] - - if key > self._size: - raise IndexError(f"Index out of range: {key} > {self._size}") return self.params[key] def __iter__(self): - return iter(self.params[: self._size]) + return iter(self.params) def __len__(self): - return self._size + return len(self._params) def __str__(self): - return f"{self.name}, {[str(item) for item in self.params[: self._size]]}" + return f"{self.name}, {[str(item) for item in self.params]}" def __repr__(self): return f"{self.__class__.__name__}(name={self.name}, length={len(self)})" def resize(self, length): - """Resize the parameter vector. - - If necessary, new elements are generated. If length is smaller than before, the - previous elements are cached and not re-generated if the vector is enlarged again. + """Resize the parameter vector. If necessary, new elements are generated. + + Note that the UUID of each :class:`.Parameter` element will be generated + deterministically given the root UUID of the ``ParameterVector`` and the index + of the element. In particular, if a ``ParameterVector`` is resized to + be smaller and then later resized to be larger, the UUID of the later + generated element at a given index will be the same as the UUID of the + previous element at that index. This is to ensure that the parameter instances do not change. + + >>> from qiskit.circuit import ParameterVector + >>> pv = ParameterVector("theta", 20) + >>> elt_19 = pv[19] + >>> rv.resize(10) + >>> rv.resize(20) + >>> pv[19] == elt_19 + True """ if length > len(self._params): root_uuid_int = self._root_uuid.int @@ -111,4 +116,5 @@ def resize(self, length): for i in range(len(self._params), length) ] ) - self._size = length + else: + del self._params[length:] diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index fdad363867a..c9f0f9af479 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -45,7 +45,7 @@ def _write_parameter_vec(file_obj, obj): struct.pack( formats.PARAMETER_VECTOR_ELEMENT_PACK, len(name_bytes), - obj._vector._size, + len(obj._vector), obj.uuid.bytes, obj._index, ) diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c9380fd0768..7cdc4ed56ab 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1368,10 +1368,8 @@ def test_parametervector_resize(self): with self.subTest("enlargen"): vec.resize(3) self.assertEqual(len(vec), 3) - # ensure we still have the same instance not a copy with the same name - # this is crucial for adding parameters to circuits since we cannot use the same - # name if the instance is not the same - self.assertIs(element, vec[1]) + # ensure we still have an element with the same uuid + self.assertEqual(element, vec[1]) self.assertListEqual([param.name for param in vec], _paramvec_names("x", 3)) def test_raise_if_sub_unknown_parameters(self): From 439de04e88546f632ac9a76fa4b6bff03da529f1 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 13 Jun 2024 12:47:34 +0200 Subject: [PATCH 114/159] Fix the possibly-used-before-assignment in pylint (#12542) * fix the possibly-used-before-assignment in pylint * qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py * test.python.quantum_info.operators.test_utils * Apply suggestions from code review Co-authored-by: Jake Lishman * https://github.com/Qiskit/qiskit/pull/12542/files#r1636214044 * RuntimeError * RuntimeError --------- Co-authored-by: Jake Lishman --- pyproject.toml | 1 - qiskit/primitives/backend_estimator.py | 2 ++ qiskit/pulse/macros.py | 9 +++++++-- qiskit/qpy/binary_io/circuits.py | 1 + qiskit/quantum_info/operators/operator.py | 20 +++++++++---------- .../operators/symplectic/sparse_pauli_op.py | 20 +++++++++---------- .../passes/basis/unroll_custom_definitions.py | 14 ++++++------- .../optimization/commutative_cancellation.py | 12 ++++++----- qiskit/visualization/circuit/matplotlib.py | 2 ++ test/benchmarks/randomized_benchmarking.py | 1 + test/benchmarks/utils.py | 2 ++ test/python/circuit/test_parameters.py | 7 ++++--- .../test_boolean_expression.py | 1 + .../test_classical_function.py | 1 + .../classical_function_compiler/test_parse.py | 1 + .../test_simulate.py | 1 + .../test_synthesis.py | 1 + .../test_tweedledum2qiskit.py | 1 + .../test_typecheck.py | 2 ++ .../visualization/test_circuit_drawer.py | 2 ++ .../visualization/test_circuit_text_drawer.py | 1 + test/python/visualization/test_gate_map.py | 1 + .../visualization/test_plot_histogram.py | 1 + tools/build_standard_commutations.py | 4 ++-- 24 files changed, 67 insertions(+), 41 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c13486c21bb..149d6c0f2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,7 +221,6 @@ disable = [ "consider-using-f-string", "no-member", # for dynamically created members "not-context-manager", - "possibly-used-before-assignment", "unnecessary-lambda-assignment", # do not want to implement "unspecified-encoding", # do not want to implement ] diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index b91ea7068be..8446c870b1f 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -65,6 +65,8 @@ def _run_circuits( max_circuits = getattr(backend.configuration(), "max_experiments", None) elif isinstance(backend, BackendV2): max_circuits = backend.max_circuits + else: + raise RuntimeError("Backend version not supported") if max_circuits: jobs = [ backend.run(circuits[pos : pos + max_circuits], **run_options) diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 1995d6d20c4..88414cfc7e9 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -124,8 +124,13 @@ def _measure_v1( for qubit in qubits: measure_groups.add(tuple(meas_map[qubit])) for measure_group_qubits in measure_groups: - if qubit_mem_slots is not None: - unused_mem_slots = set(measure_group_qubits) - set(qubit_mem_slots.values()) + + unused_mem_slots = ( + set() + if qubit_mem_slots is None + else set(measure_group_qubits) - set(qubit_mem_slots.values()) + ) + try: default_sched = inst_map.get(measure_name, measure_group_qubits) except exceptions.PulseError as ex: diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 25103e7b4c2..db53defbcfa 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -446,6 +446,7 @@ def _parse_custom_operation( ) = custom_operations[gate_name] else: type_str, num_qubits, num_clbits, definition = custom_operations[gate_name] + base_gate_raw = ctrl_state = num_ctrl_qubits = None # Strip the trailing "_{uuid}" from the gate name if the version >=11 if version >= 11: gate_name = "_".join(gate_name.split("_")[:-1]) diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 41eac356357..016e337f082 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -414,6 +414,8 @@ def from_circuit( from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern + op = Operator(circuit) + if initial_layout is not None: input_qubits = [None] * len(layout.input_qubit_mapping) for q, p in layout.input_qubit_mapping.items(): @@ -421,22 +423,18 @@ def from_circuit( initial_permutation = initial_layout.to_permutation(input_qubits) initial_permutation_inverse = _inverse_pattern(initial_permutation) + op = op.apply_permutation(initial_permutation, True) - if final_layout is not None: + if final_layout is not None: + final_permutation = final_layout.to_permutation(circuit.qubits) + final_permutation_inverse = _inverse_pattern(final_permutation) + op = op.apply_permutation(final_permutation_inverse, False) + op = op.apply_permutation(initial_permutation_inverse, False) + elif final_layout is not None: final_permutation = final_layout.to_permutation(circuit.qubits) final_permutation_inverse = _inverse_pattern(final_permutation) - - op = Operator(circuit) - - if initial_layout: - op = op.apply_permutation(initial_permutation, True) - - if final_layout: op = op.apply_permutation(final_permutation_inverse, False) - if initial_layout: - op = op.apply_permutation(initial_permutation_inverse, False) - return op def is_unitary(self, atol=None, rtol=None): diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index f8d25a6e8da..6204189be44 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -135,19 +135,19 @@ def __init__( pauli_list = PauliList(data.copy() if copy and hasattr(data, "copy") else data) - if isinstance(coeffs, np.ndarray): - dtype = object if coeffs.dtype == object else complex - elif coeffs is not None: - if not isinstance(coeffs, (np.ndarray, Sequence)): - coeffs = [coeffs] - if any(isinstance(coeff, ParameterExpression) for coeff in coeffs): - dtype = object - else: - dtype = complex - if coeffs is None: coeffs = np.ones(pauli_list.size, dtype=complex) else: + if isinstance(coeffs, np.ndarray): + dtype = object if coeffs.dtype == object else complex + else: + if not isinstance(coeffs, Sequence): + coeffs = [coeffs] + if any(isinstance(coeff, ParameterExpression) for coeff in coeffs): + dtype = object + else: + dtype = complex + coeffs_asarray = np.asarray(coeffs, dtype=dtype) coeffs = ( coeffs_asarray.copy() diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 2a95f540f88..a54e4bfcb00 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -60,9 +60,9 @@ def run(self, dag): if self._basis_gates is None and self._target is None: return dag + device_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} if self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} - device_insts = basic_insts | set(self._basis_gates) + device_insts |= set(self._basis_gates) for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): @@ -77,14 +77,14 @@ def run(self, dag): controlled_gate_open_ctrl = isinstance(node.op, ControlledGate) and node.op._open_ctrl if not controlled_gate_open_ctrl: - inst_supported = ( - self._target.instruction_supported( + if self._target is not None: + inst_supported = self._target.instruction_supported( operation_name=node.op.name, qargs=tuple(dag.find_bit(x).index for x in node.qargs), ) - if self._target is not None - else node.name in device_insts - ) + else: + inst_supported = node.name in device_insts + if inst_supported or self._equiv_lib.has_entry(node.op): continue try: diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 396186fa95c..68d40f3650a 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -16,7 +16,6 @@ import numpy as np from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passes.optimization.commutation_analysis import CommutationAnalysis @@ -72,9 +71,6 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. - - Raises: - TranspilerError: when the 1-qubit rotation gates are not found """ var_z_gate = None z_var_gates = [gate for gate in dag.count_ops().keys() if gate in self._var_z_map] @@ -146,7 +142,7 @@ def run(self, dag): or len(current_node.qargs) != 1 or current_node.qargs[0] != run_qarg ): - raise TranspilerError("internal error") + raise RuntimeError("internal error") if current_node.name in ["p", "u1", "rz", "rx"]: current_angle = float(current_node.op.params[0]) @@ -156,6 +152,10 @@ def run(self, dag): current_angle = np.pi / 4 elif current_node.name == "s": current_angle = np.pi / 2 + else: + raise RuntimeError( + f"Angle for operation {current_node.name } is not defined" + ) # Compose gates total_angle = current_angle + total_angle @@ -167,6 +167,8 @@ def run(self, dag): new_op = var_z_gate(total_angle) elif cancel_set_key[0] == "x_rotation": new_op = RXGate(total_angle) + else: + raise RuntimeError("impossible case") new_op_phase = 0 if np.mod(total_angle, (2 * np.pi)) > _CUTOFF_PRECISION: diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index b4252065006..0076073fb8e 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -1584,6 +1584,8 @@ def _flow_op_gate(self, node, node_data, glob_data): flow_text = " For" elif isinstance(node.op, SwitchCaseOp): flow_text = "Switch" + else: + flow_text = node.op.name # Some spacers. op_spacer moves 'Switch' back a bit for alignment, # expr_spacer moves the expr over to line up with 'Switch' and diff --git a/test/benchmarks/randomized_benchmarking.py b/test/benchmarks/randomized_benchmarking.py index f3c3d18e9f9..9847c928ad7 100644 --- a/test/benchmarks/randomized_benchmarking.py +++ b/test/benchmarks/randomized_benchmarking.py @@ -105,6 +105,7 @@ def clifford_2_qubit_circuit(num): qc = QuantumCircuit(2) if vals[0] == 0 or vals[0] == 3: (form, i0, i1, j0, j1, p0, p1) = vals + k0, k1 = (None, None) else: (form, i0, i1, j0, j1, k0, k1, p0, p1) = vals if i0 == 1: diff --git a/test/benchmarks/utils.py b/test/benchmarks/utils.py index d932e8d6a0c..bbd7d0a9af8 100644 --- a/test/benchmarks/utils.py +++ b/test/benchmarks/utils.py @@ -126,6 +126,8 @@ def random_circuit( operation = rng.choice(two_q_ops) elif num_operands == 3: operation = rng.choice(three_q_ops) + else: + raise RuntimeError("not supported number of operands") if operation in one_param: num_angles = 1 elif operation in two_param: diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 7cdc4ed56ab..c86deee4287 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1091,7 +1091,7 @@ def test_decompose_propagates_bound_parameters(self, target_type, parameter_type if target_type == "gate": inst = qc.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": inst = qc.to_instruction() qc2 = QuantumCircuit(1) @@ -1132,7 +1132,7 @@ def test_decompose_propagates_deeply_bound_parameters(self, target_type, paramet if target_type == "gate": inst = qc1.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": inst = qc1.to_instruction() qc2 = QuantumCircuit(1) @@ -1188,7 +1188,7 @@ def test_executing_parameterized_instruction_bound_early(self, target_type): if target_type == "gate": sub_inst = sub_qc.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": sub_inst = sub_qc.to_instruction() unbound_qc = QuantumCircuit(2, 1) @@ -1405,6 +1405,7 @@ def _paramvec_names(prefix, length): @ddt class TestParameterExpressions(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Test expressions of Parameters.""" # supported operations dictionary operation : accuracy (0=exact match) diff --git a/test/python/classical_function_compiler/test_boolean_expression.py b/test/python/classical_function_compiler/test_boolean_expression.py index afdc91fdd04..40a01b154c6 100644 --- a/test/python/classical_function_compiler/test_boolean_expression.py +++ b/test/python/classical_function_compiler/test_boolean_expression.py @@ -28,6 +28,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") @ddt class TestBooleanExpression(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Test boolean expression.""" @data( diff --git a/test/python/classical_function_compiler/test_classical_function.py b/test/python/classical_function_compiler/test_classical_function.py index d4a0bf66d49..e385745952e 100644 --- a/test/python/classical_function_compiler/test_classical_function.py +++ b/test/python/classical_function_compiler/test_classical_function.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestOracleDecomposition(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests ClassicalFunction.decomposition.""" def test_grover_oracle(self): diff --git a/test/python/classical_function_compiler/test_parse.py b/test/python/classical_function_compiler/test_parse.py index 15862ca71b3..9da93873c70 100644 --- a/test/python/classical_function_compiler/test_parse.py +++ b/test/python/classical_function_compiler/test_parse.py @@ -25,6 +25,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestParseFail(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests bad_examples with the classicalfunction parser.""" def assertExceptionMessage(self, context, message): diff --git a/test/python/classical_function_compiler/test_simulate.py b/test/python/classical_function_compiler/test_simulate.py index 65399de82d0..f7c6ef3dd16 100644 --- a/test/python/classical_function_compiler/test_simulate.py +++ b/test/python/classical_function_compiler/test_simulate.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") @ddt class TestSimulate(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests LogicNetwork.simulate method""" @data(*utils.example_list()) diff --git a/test/python/classical_function_compiler/test_synthesis.py b/test/python/classical_function_compiler/test_synthesis.py index 3b8890d986c..1d44b58882f 100644 --- a/test/python/classical_function_compiler/test_synthesis.py +++ b/test/python/classical_function_compiler/test_synthesis.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestSynthesis(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests ClassicalFunction.synth method.""" def test_grover_oracle(self): diff --git a/test/python/classical_function_compiler/test_tweedledum2qiskit.py b/test/python/classical_function_compiler/test_tweedledum2qiskit.py index 32bd9485fdc..ff9b73a5b55 100644 --- a/test/python/classical_function_compiler/test_tweedledum2qiskit.py +++ b/test/python/classical_function_compiler/test_tweedledum2qiskit.py @@ -29,6 +29,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTweedledum2Qiskit(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests qiskit.transpiler.classicalfunction.utils.tweedledum2qiskit function.""" def test_x(self): diff --git a/test/python/classical_function_compiler/test_typecheck.py b/test/python/classical_function_compiler/test_typecheck.py index 36b64ce4fd4..ffe57cc3d4b 100644 --- a/test/python/classical_function_compiler/test_typecheck.py +++ b/test/python/classical_function_compiler/test_typecheck.py @@ -25,6 +25,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTypeCheck(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests classicalfunction compiler type checker (good examples).""" def test_id(self): @@ -74,6 +75,7 @@ def test_bool_or(self): @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTypeCheckFail(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests classicalfunction compiler type checker (bad examples).""" def assertExceptionMessage(self, context, message): diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index f02f1ad1143..e6b430c4ee8 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -55,6 +55,7 @@ def test_default_output(self): @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "Skipped because matplotlib is not available") def test_mpl_config_with_path(self): + # pylint: disable=possibly-used-before-assignment # It's too easy to get too nested in a test with many context managers. tempdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with self.addCleanup(tempdir.cleanup) @@ -128,6 +129,7 @@ def test_latex_unsupported_image_format_error_message(self): @_latex_drawer_condition def test_latex_output_file_correct_format(self): + # pylint: disable=possibly-used-before-assignment with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "latex"}): circuit = QuantumCircuit() filename = "file.gif" diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 3f018c08510..2a0a61c7904 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -185,6 +185,7 @@ def test_text_no_pager(self): class TestTextDrawerGatesInCircuit(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Gate by gate checks in different settings.""" def test_text_measure_cregbundle(self): diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index dd3a479ba65..bf9b1ca80d7 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -45,6 +45,7 @@ @unittest.skipUnless(optionals.HAS_PIL, "PIL not available") @unittest.skipUnless(optionals.HAS_SEABORN, "seaborn not available") class TestGateMap(QiskitVisualizationTestCase): + # pylint: disable=possibly-used-before-assignment """visual tests for plot_gate_map""" backends = [Fake5QV1(), Fake20QV1(), Fake7QPulseV1()] diff --git a/test/python/visualization/test_plot_histogram.py b/test/python/visualization/test_plot_histogram.py index 7c530851326..2668f3ff679 100644 --- a/test/python/visualization/test_plot_histogram.py +++ b/test/python/visualization/test_plot_histogram.py @@ -28,6 +28,7 @@ @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") class TestPlotHistogram(QiskitVisualizationTestCase): + # pylint: disable=possibly-used-before-assignment """Qiskit plot_histogram tests.""" def test_different_counts_lengths(self): diff --git a/tools/build_standard_commutations.py b/tools/build_standard_commutations.py index 72798f0eb4b..56c452b11ce 100644 --- a/tools/build_standard_commutations.py +++ b/tools/build_standard_commutations.py @@ -102,12 +102,12 @@ def _generate_commutation_dict(considered_gates: List[Gate] = None) -> dict: commutation_relation = cc.commute( op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits=4 ) + + gate_pair_commutation[relative_placement] = commutation_relation else: pass # TODO - gate_pair_commutation[relative_placement] = commutation_relation - commutations[gate0.name, gate1.name] = gate_pair_commutation return commutations From f304a4b4f8add43defafeabbbbd32baefa30aa7c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 13 Jun 2024 06:48:40 -0400 Subject: [PATCH 115/159] Add infrastructure for gates, instruction, and operations in Rust (#12459) * Add infrastructure for gates, instruction, and operations in Rust This commit adds a native representation of Gates, Instruction, and Operations to rust's circuit module. At a high level this works by either wrapping the Python object in a rust wrapper struct that tracks metadata about the operations (name, num_qubits, etc) and then for other details it calls back to Python to get dynamic details like the definition, matrix, etc. For standard library gates like Swap, CX, H, etc this replaces the on-circuit representation with a new rust enum StandardGate. The enum representation is much more efficient and has a minimal memory footprint (just the enum variant and then any parameters or other mutable state stored in the circuit instruction). All the gate properties such as the matrix, definiton, name, etc are statically defined in rust code based on the enum variant (which represents the gate). The use of an enum to represent standard gates does mean a change in what we store on a CircuitInstruction. To represent a standard gate fully we need to store the mutable properties of the existing Gate class on the circuit instruction as the gate by itself doesn't contain this detail. That means, the parameters, label, unit, duration, and condition are added to the rust side of circuit instrucion. However no Python side access methods are added for these as they're internal only to the Rust code. In Qiskit 2.0 to simplify this storage we'll be able to drop, unit, duration, and condition from the api leaving only label and parameters. But for right now we're tracking all of the fields. To facilitate working with circuits and gates full from rust the setting the `operation` attribute of a `CircuitInstruction` object now transltates the python object to an internal rust representation. For standard gates this translates it to the enum form described earlier, and for other circuit operations 3 new Rust structs: PyGate, PyInstruction, and PyOperation are used to wrap the underlying Python object in a Rust api. These structs cache some commonly accessed static properties of the operation, such as the name, number of qubits, etc. However for dynamic pieces, such as the definition or matrix, callback to python to get a rust representation for those. Similarly whenever the `operation` attribute is accessed from Python it converts it back to the normal Python object representation. For standard gates this involves creating a new instance of a Python object based on it's internal rust representation. For the wrapper structs a reference to the wrapped PyObject is returned. To manage the 4 variants of operation (`StandardGate`, `PyGate`, `PyInstruction`, and `PyOperation`) a new Rust trait `Operation` is created that defines a standard interface for getting the properties of a given circuit operation. This common interface is implemented for the 4 variants as well as the `OperationType` enum which wraps all 4 (and is used as the type for `CircuitInstruction.operation` in the rust code. As everything in the `QuantumCircuit` data model is quite coupled moving the source of truth for the operations to exist in Rust means that more of the underlying `QuantumCircuit`'s responsibility has to move to Rust as well. Primarily this involves the `ParameterTable` which was an internal class for tracking which instructions in the circuit have a `ParameterExpression` parameter so that when we go to bind parameters we can lookup which operations need to be updated with the bind value. Since the representation of those instructions now lives in Rust and Python only recieves a ephemeral copy of the instructions the ParameterTable had to be reimplemented in Rust to track the instructions. This new parameter table maps the Parameter's uuid (as a u128) as a unique identifier for each parameter and maps this to a positional index in the circuit data to the underlying instruction using that parameter. This is a bit different from the Python parameter table which was mapping a parameter object to the id of the operation object using that parmaeter. This also leads to a difference in the binding mechanics as the parameter assignment was done by reference in the old model, but now we need to update the entire instruction more explicitly in rust. Additionally, because the global phase of a circuit can be parameterized the ownership of global phase is moved from Python into Rust in this commit as well. After this commit the only properties of a circuit that are not defined in Rust for the source of truth are the bits (and vars) of the circuit, and when creating circuits from rust this is what causes a Python interaction to still be required. This commit does not translate the full standard library of gates as that would make the pull request huge, instead this adds the basic infrastructure for having a more efficient standard gate representation on circuits. There will be follow up pull requests to add the missing gates and round out support in rust. The goal of this pull request is primarily to add the infrastructure for representing the full circuit model (and dag model in the future) in rust. By itself this is not expected to improve runtime performance (if anything it will probably hurt performance because of extra type conversions) but it is intended to enable writing native circuit manipulations in Rust, including transpiler passes without needing involvement from Python. Longer term this should greatly improve the runtime performance and reduce the memory overhead of Qiskit. But, this is just an early step towards that goal, and is more about unlocking the future capability. The next steps after this commit are to finish migrating the standard gate library and also update the `QuantumCircuit` methods to better leverage the more complete rust representation (which should help offset the performance penalty introduced by this). Fixes: #12205 * Fix Python->Rust Param conversion This commit adds a custom implementation of the FromPyObject trait for the Param enum. Previously, the Param trait derived it's impl of the trait, but this logic wasn't perfect. In cases whern a ParameterExpression was effectively a constant (such as `0 * x`) the trait's attempt to coerce to a float first would result in those ParameterExpressions being dropped from the circuit at insertion time. This was a change in behavior from before having gates in Rust as the parameters would disappear from the circuit at insertion time instead of at bind time. This commit fixes this by having a custom impl for FromPyObject that first tries to figure out if the parameter is a ParameterExpression (or a QuantumCircuit) by using a Python isinstance() check, then tries to extract it as a float, and finally stores a non-parameter object; which is a new variant in the Param enum. This new variant also lets us simplify the logic around adding gates to the parameter table as we're able to know ahead of time which gate parameters are `ParameterExpression`s and which are other objects (and don't need to be tracked in the parameter table. Additionally this commit tweaks two tests, the first is test.python.circuit.library.test_nlocal.TestNLocal.test_parameters_setter which was adjusted in the previous commit to workaround the bug fixed by this commit. The second is test.python.circuit.test_parameters which was testing that a bound ParameterExpression with a value of 0 defaults to an int which was a side effect of passing an int input to symengine for the bind value and not part of the api and didn't need to be checked. This assertion was removed from the test because the rust representation is only storing f64 values for the numeric parameters and it is never an int after binding from the Python perspective it isn't any different to have float(0) and int(0) unless you explicit isinstance check like the test previously was. * Fix qasm3 exporter for std gates without stdgates.inc This commit fixes the handling of standard gates in Qiskit when the user specifies excluding the use of the stdgates.inc file from the exported qasm. Previously the object id of the standard gates were used to maintain a lookup table of the global definitions for all the standard gates explicitly in the file. However, the rust refactor means that every time the exporter accesses `circuit.data[x].operation` a new instance is returned. This means that on subsequent lookups for the definition the gate definitions are never found. To correct this issue this commit adds to the lookup table a fallback of the gate name + parameters to do the lookup for. This should be unique for any standard gate and not interfere with the previous logic that's still in place and functional for other custom gate definitions. While this fixes the logic in the exporter the test is still failing because the test is asserting the object ids are the same in the qasm3 file, which isn't the case anymore. The test will be updated in a subsequent commit to validate the qasm3 file is correct without using a hardcoded object id. * Fix base scheduler analysis pass duration setting When ALAPScheduleAnalysis and ASAPScheduleAnalysis were setting the duration of a gate they were doing `node.op.duration = duration` this wasn't always working because if `node.op` was a standard gate it returned a new Python object created from the underlying rust representation. This commit fixes the passes so that they modify the duration and then explicit set the operation to update it's rust representation. * Fix python lint * Fix last failing qasm3 test for std gates without stdgates.inc While the logic for the qasm3 exporter was fixed in commit a6e69ba4c99cdd2fa50ed10399cada322bc88903 to handle the edge case of a user specifying that the qasm exporter does not use the stdgates.inc include file in the output, but also has qiskit's standard gates in their circuit being exported. The one unit test to provide coverage for that scenario was not passing because when an id was used for the gate definitions in the qasm3 file it was being referenced against a temporary created by accessing a standard gate from the circuit and the ids weren't the same so the reference string didn't match what the exporter generated. This commit fixes this by changing the test to not do an exact string comparison, but instead a line by line comparison that either does exact equality check or a regex search for the expected line and the ids are checked as being any 15 character integer. * Remove superfluous comment * Cache imported classes with GILOnceCell * Remove unused python variables * Add missing file * Update QuantumCircuit gate methods to bypass Python object This commit updates the QuantumCircuit gate methods which add a given gate to the circuit to bypass the python gate object creation and directly insert a rust representation of the gate. This avoids a conversion in the rust side of the code. While in practice this is just the Python side object creation and a getattr for the rust code to determine it's a standard gate that we're skipping. This may add up over time if there are a lot of gates being created by the method. To accomplish this the rust code handling the mapping of rust StandardGate variants to the Python classes that represent those gates needed to be updated as well. By bypassing the python object creation we need a fallback to populate the gate class for when a user access the operation object from Python. Previously this mapping was only being populated at insertion time and if we never insert the python object (for a circuit created only via the methods) then we need a way to find what the gate class is. A static lookup table of import paths and class names are added to `qiskit_circuit::imports` module to faciliate this and helper functions are added to facilitate interacting with the class objects that represent each gate. * Deduplicate gate matrix definitions * Fix lint * Attempt to fix qasm3 test failure * Add compile time option to cache py gate returns for rust std gates This commit adds a new rust crate feature flag for the qiskit-circuits and qiskit-pyext that enables caching the output from CircuitInstruction.operation to python space. Previously, for memory efficiency we were reconstructing the python object on demand for every access. This was to avoid carrying around an extra pointer and keeping the ephemeral python object around longer term if it's only needed once. But right now nothing is directly using the rust representation yet and everything is accessing via the python interface, so recreating gate objects on the fly has a huge performance penalty. To avoid that this adds caching by default as a temporary solution to avoid this until we have more usage of the rust representation of gates. There is an inherent tension between an optimal rust representation and something that is performant for Python access and there isn't a clear cut answer on which one is better to optimize for. A build time feature lets the user pick, if what we settle on for the default doesn't agree with their priorities or use case. Personally I'd like to see us disable the caching longer term (hopefully before releasing this functionality), but that's dependent on a sufficent level of usage from rust superseding the current Python space usage in the core of Qiskit. * Add num_nonlocal_gates implementation in rust This commit adds a native rust implementation to rust for the num_nonlocal_gates method on QuantumCircuit. Now that we have a rust representation of gates it is potentially faster to do the count because the iteration and filtering is done rust side. * Performance tuning circuit construction This commit fixes some performance issues with the addition of standard gates to a circuit. To workaround potential reference cycles in Python when calling rust we need to check the parameters of the operation. This was causing our fast path for standard gates to access the `operation` attribute to get the parameters. This causes the gate to be eagerly constructed on the getter. However, the reference cycle case can only happen in situations without a standard gate, and the fast path for adding standard gates directly won't need to run this so a skip is added if we're adding a standard gate. * Add back validation of parameters on gate methods In the previous commit a side effect of the accidental eager operation creation was that the parameter input for gates were being validated by that. By fixing that in the previous commit the validation of input parameters on the circuit methods was broken. This commit fixes that oversight and adds back the validation. * Skip validation on gate creation from rust * Offload operation copying to rust This commit fixes a performance regression in the `QuantumCircuit.copy()` method which was previously using Python to copy the operations which had extra overhead to go from rust to python and vice versa. This moves that logic to exist in rust and improve the copy performance. * Fix lint * Perform deepcopy in rust This commit moves the deepcopy handling to occur solely in Rust. Previously each instruction would be directly deepcopied by iterating over the circuit data. However, we can do this rust side now and doing this is more efficient because while we need to rely on Python to run a deepcopy we can skip it for the Rust standard gates and rely on Rust to copy those gates. * Fix QuantumCircuit.compose() performance regression This commit fixes a performance regression in the compose() method. This was caused by the checking for classical conditions in the method requiring eagerly converting all standard gates to a Python object. This changes the logic to do this only if we know we have a condition (which we can determine Python side now). * Fix map_ops test case with no caching case * Fix typos in docs This commit fixes several docs typos that were caught during code review. Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> * Shrink memory usage for extra mutable instruction state This commit changes how we store the extra mutable instruction state (condition, duration, unit, and label) for each `CircuitInstruction` and `PackedInstruction` in the circuit. Previously it was all stored as separate `Option` fields on the struct, which required at least a pointer's width for each field which was wasted space the majority of the time as using these fields are not common. To optimize the memory layout of the struct this moves these attributes to a new struct which is put in an `Option>` which reduces it from 4 pointer widths down to 1 per object. This comes from extra runtime cost from the extra layer of pointer indirection but as this is the uncommon path this tradeoff is fine. * Remove Option<> from params field in CircuitInstruction This commit removes the Option<> from the params field in CircuitInstruction. There is no real distinction between an empty vec and None in this case, so the option just added another layer in the API that we didn't need to deal with. Also depending on the memory alignment using an Option might have ended up in a little extra memory usage too, so removing it removes that potential source of overhead. * Eagerly construct rust python wrappers in .append() This commit updates the Python code in QuantumCircuit.append() method to eagerly construct the rust wrapper objects for python defined circuit operations. * Simplify code around handling python errors in rust * Revert "Skip validation on gate creation from rust" This reverts commit 2f81bde8bf32b06b4165048896eabdb36470814d. The validation skipping was unsound in some cases and could lead to invalid circuit being generated. If we end up needing this as an optimization we can remove this in the future in a follow-up PR that explores this in isolation. * Temporarily use git for qasm3 import In Qiskit/qiskit-qasm3-import#34 the issue we're hitting caused by qiskit-qasm3-import using the private circuit attributes removed in this PR was fixed. This commit temporarily moves to installing it from git so we can fully run CI. When qiskit-qasm3-import is released we should revert this commit. * Fix lint * Fix lint for real (we really need to use a py312 compatible version of pylint) * Fix test failure caused by incorrect lint fix * Relax trait-method typing requirements * Encapsulate `GILOnceCell` initialisers to local logic * Simplify Interface for building circuit of standard gates in rust * Simplify complex64 creation in gate_matrix.rs This just switches Complex64::new(re, im) to be c64(re, im) to reduce the amount of typing. c64 needs to be defined inplace so it can be a const fn. * Simplify initialization of array of elements that are not Copy (#28) * Simplify initialization of array of elements that are not Copy * Only generate array when necessary * Fix doc typos Co-authored-by: Kevin Hartman * Add conversion trait for OperationType -> OperationInput and simplify CircuitInstruction::replace() * Use destructuring for operation_type_to_py extra attr handling * Simplify trait bounds for map_indices() The map_indices() method previously specified both Iterator and ExactSizeIterator for it's trait bounds, but Iterator is a supertrait of ExactSizeIterator and we don't need to explicitly list both. This commit removes the duplicate trait bound. * Make Qubit and Clbit newtype member public As we start to use Qubit and Clbit for creating circuits from accelerate and other crates in the Qiskit workspace we need to be able to create instances of them. However, the newtype member BitType was not public which prevented creating new Qubits. This commit fixes this by making it public. * Use snakecase for gate matrix names * Remove pointless underscore prefix * Use downcast instead of bound * Rwork _append reference cycle handling This commit reworks the multiple borrow handling in the _append() method to leveraging `Bound.try_borrow()` to return a consistent error message if we're unable to borrow a CircuitInstruction in the rust code meaning there is a cyclical reference in the code. Previously we tried to detect this cycle up-front which added significant overhead for a corner case. * Make CircuitData.global_phase_param_index a class attr * Use &[Param] instead of &SmallVec<..> for operation_type_and_data_to_py * Have get_params_unsorted return a set * Use lookup table for static property methods of StandardGate * Use PyTuple::empty_bound() * Fix lint * Add missing test method docstring * Reuse allocations in parameter table update * Remove unnecessary global phase zeroing * Move manually set params to a separate function * Fix release note typo * Use constant for global-phase index * Switch requirement to release version --------- Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Co-authored-by: Jake Lishman Co-authored-by: John Lapeyre Co-authored-by: Kevin Hartman --- .github/workflows/tests.yml | 9 + CONTRIBUTING.md | 12 + Cargo.lock | 4 + Cargo.toml | 5 + crates/accelerate/Cargo.toml | 8 +- crates/accelerate/src/isometry.rs | 2 +- crates/accelerate/src/two_qubit_decompose.rs | 68 +- crates/circuit/Cargo.toml | 15 +- crates/circuit/README.md | 63 ++ crates/circuit/src/bit_data.rs | 13 +- crates/circuit/src/circuit_data.rs | 911 +++++++++++++++++- crates/circuit/src/circuit_instruction.rs | 698 +++++++++++++- crates/circuit/src/dag_node.rs | 86 +- crates/circuit/src/gate_matrix.rs | 224 +++++ crates/circuit/src/imports.rs | 168 ++++ crates/circuit/src/interner.rs | 12 +- crates/circuit/src/lib.rs | 13 +- crates/circuit/src/operations.rs | 786 +++++++++++++++ crates/circuit/src/packed_instruction.rs | 25 - crates/circuit/src/parameter_table.rs | 173 ++++ crates/pyext/Cargo.toml | 1 + qiskit/circuit/controlflow/builder.py | 8 +- qiskit/circuit/instruction.py | 1 + qiskit/circuit/instructionset.py | 9 +- qiskit/circuit/library/blueprintcircuit.py | 8 +- qiskit/circuit/library/standard_gates/ecr.py | 3 + .../library/standard_gates/global_phase.py | 3 + qiskit/circuit/library/standard_gates/h.py | 3 + qiskit/circuit/library/standard_gates/i.py | 3 + qiskit/circuit/library/standard_gates/p.py | 3 + qiskit/circuit/library/standard_gates/rx.py | 3 + qiskit/circuit/library/standard_gates/ry.py | 3 + qiskit/circuit/library/standard_gates/rz.py | 3 + qiskit/circuit/library/standard_gates/swap.py | 3 + qiskit/circuit/library/standard_gates/sx.py | 3 + qiskit/circuit/library/standard_gates/u.py | 3 + qiskit/circuit/library/standard_gates/x.py | 7 + qiskit/circuit/library/standard_gates/y.py | 5 + qiskit/circuit/library/standard_gates/z.py | 5 + qiskit/circuit/parametertable.py | 191 +--- qiskit/circuit/quantumcircuit.py | 419 ++++---- qiskit/circuit/quantumcircuitdata.py | 4 +- qiskit/converters/circuit_to_instruction.py | 12 +- qiskit/qasm3/exporter.py | 10 +- .../operators/dihedral/dihedral.py | 3 +- .../padding/dynamical_decoupling.py | 16 +- .../scheduling/scheduling/base_scheduler.py | 5 +- .../passes/scheduling/time_unit_conversion.py | 7 +- .../circuit-gates-rust-5c6ab6c58f7fd2c9.yaml | 79 ++ requirements-optional.txt | 2 +- setup.py | 12 + .../circuit/library/test_blueprintcircuit.py | 6 +- test/python/circuit/test_circuit_data.py | 25 +- .../python/circuit/test_circuit_operations.py | 2 +- test/python/circuit/test_compose.py | 3 +- test/python/circuit/test_instructions.py | 12 +- test/python/circuit/test_isometry.py | 1 - test/python/circuit/test_parameters.py | 191 +--- test/python/circuit/test_rust_equivalence.py | 143 +++ test/python/qasm3/test_export.py | 201 ++-- 60 files changed, 3780 insertions(+), 936 deletions(-) create mode 100644 crates/circuit/src/gate_matrix.rs create mode 100644 crates/circuit/src/imports.rs create mode 100644 crates/circuit/src/operations.rs delete mode 100644 crates/circuit/src/packed_instruction.rs create mode 100644 crates/circuit/src/parameter_table.rs create mode 100644 releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml create mode 100644 test/python/circuit/test_rust_equivalence.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08530adfd4f..20e40dec982 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,6 +36,15 @@ jobs: python -m pip install -U -r requirements.txt -c constraints.txt python -m pip install -U -r requirements-dev.txt -c constraints.txt python -m pip install -c constraints.txt -e . + if: matrix.python-version == '3.10' + env: + QISKIT_NO_CACHE_GATES: 1 + - name: 'Install dependencies' + run: | + python -m pip install -U -r requirements.txt -c constraints.txt + python -m pip install -U -r requirements-dev.txt -c constraints.txt + python -m pip install -c constraints.txt -e . + if: matrix.python-version == '3.12' - name: 'Install optionals' run: | python -m pip install -r requirements-optional.txt -c constraints.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c75b5175001..4641c7878fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,18 @@ Note that in order to run `python setup.py ...` commands you need have build dependency packages installed in your environment, which are listed in the `pyproject.toml` file under the `[build-system]` section. +### Compile time options + +When building qiskit from source there are options available to control how +Qiskit is built. Right now the only option is if you set the environment +variable `QISKIT_NO_CACHE_GATES=1` this will disable runtime caching of +Python gate objects when accessing them from a `QuantumCircuit` or `DAGCircuit`. +This makes a tradeoff between runtime performance for Python access and memory +overhead. Caching gates will result in better runtime for users of Python at +the cost of increased memory consumption. If you're working with any custom +transpiler passes written in python or are otherwise using a workflow that +repeatedly accesses the `operation` attribute of a `CircuitInstruction` or `op` +attribute of `DAGOpNode` enabling caching is recommended. ## Issues and pull requests diff --git a/Cargo.lock b/Cargo.lock index cf8e4f365df..aefa3c932a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,7 +1196,11 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", + "ndarray", + "num-complex", + "numpy", "pyo3", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2827b2206f4..13f43cfabcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ license = "Apache-2.0" [workspace.dependencies] indexmap.version = "2.2.6" hashbrown.version = "0.14.0" +num-complex = "0.4" +ndarray = "^0.15.6" +numpy = "0.21.0" +smallvec = "1.13" + # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python # distributions). We only activate that feature when building the C extension module; we still need diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 63be9ad90b4..d9865d54543 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -11,13 +11,13 @@ doctest = false [dependencies] rayon = "1.10" -numpy = "0.21.0" +numpy.workspace = true rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" ahash = "0.8.11" num-traits = "0.2" -num-complex = "0.4" +num-complex.workspace = true num-bigint = "0.4" rustworkx-core = "0.14" faer = "0.19.0" @@ -25,7 +25,7 @@ itertools = "0.13.0" qiskit-circuit.workspace = true [dependencies.smallvec] -version = "1.13" +workspace = true features = ["union"] [dependencies.pyo3] @@ -33,7 +33,7 @@ workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] [dependencies.ndarray] -version = "^0.15.6" +workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index a4e83358a7d..a3a8be38dae 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -23,7 +23,7 @@ use itertools::Itertools; use ndarray::prelude::*; use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; -use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; /// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or /// basis_state=1 respectively diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 5e833bd86fd..f93eb2a8d99 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -51,6 +51,7 @@ use rand::prelude::*; use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; +use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; use qiskit_circuit::SliceOrInt; const PI2: f64 = PI / 2.0; @@ -60,11 +61,6 @@ const TWO_PI: f64 = 2.0 * PI; const C1: c64 = c64 { re: 1.0, im: 0.0 }; -pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; - static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [ [ Complex64::new(1.0, 0.), @@ -342,54 +338,6 @@ fn rz_matrix(theta: f64) -> Array2 { ] } -static HGATE: [[Complex64; 2]; 2] = [ - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(-FRAC_1_SQRT_2, 0.), - ], -]; - -static CXGATE: [[Complex64; 4]; 4] = [ - [ - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], -]; - -static SXGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0.5, 0.5), Complex64::new(0.5, -0.5)], - [Complex64::new(0.5, -0.5), Complex64::new(0.5, 0.5)], -]; - -static XGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(1., 0.), Complex64::new(0., 0.)], -]; - fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); let phase = Complex64::new(0., global_phase).exp(); @@ -402,10 +350,10 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2< // sequence. If we get a different gate this is getting called // by something else and is invalid. let gate_matrix = match inst.0.as_ref() { - "sx" => aview2(&SXGATE).to_owned(), + "sx" => aview2(&SX_GATE).to_owned(), "rz" => rz_matrix(inst.1[0]), - "cx" => aview2(&CXGATE).to_owned(), - "x" => aview2(&XGATE).to_owned(), + "cx" => aview2(&CX_GATE).to_owned(), + "x" => aview2(&X_GATE).to_owned(), _ => unreachable!("Undefined gate"), }; (gate_matrix, &inst.2) @@ -1481,7 +1429,7 @@ impl TwoQubitBasisDecomposer { } else { euler_matrix_q0 = rz_matrix(euler_q0[0][2] + euler_q0[1][0]).dot(&euler_matrix_q0); } - euler_matrix_q0 = aview2(&HGATE).dot(&euler_matrix_q0); + euler_matrix_q0 = aview2(&H_GATE).dot(&euler_matrix_q0); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q0.view(), 0); let rx_0 = rx_matrix(euler_q1[0][0]); @@ -1489,7 +1437,7 @@ impl TwoQubitBasisDecomposer { let rx_1 = rx_matrix(euler_q1[0][2] + euler_q1[1][0]); let mut euler_matrix_q1 = rz.dot(&rx_0); euler_matrix_q1 = rx_1.dot(&euler_matrix_q1); - euler_matrix_q1 = aview2(&HGATE).dot(&euler_matrix_q1); + euler_matrix_q1 = aview2(&H_GATE).dot(&euler_matrix_q1); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q1.view(), 1); gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); @@ -1550,12 +1498,12 @@ impl TwoQubitBasisDecomposer { return None; } gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); - let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rx_matrix(euler_q0[3][1]).dot(&euler_matrix); euler_matrix = rz_matrix(euler_q0[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 0); - let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rz_matrix(euler_q1[3][1]).dot(&euler_matrix); euler_matrix = rx_matrix(euler_q1[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 1); diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc3..dd7e878537d 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -11,4 +11,17 @@ doctest = false [dependencies] hashbrown.workspace = true -pyo3.workspace = true +num-complex.workspace = true +ndarray.workspace = true +numpy.workspace = true + +[dependencies.pyo3] +workspace = true +features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] + +[dependencies.smallvec] +workspace = true +features = ["union"] + +[features] +cache_pygates = [] diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b9375c9f99d..bbb4e54651a 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -4,3 +4,66 @@ The Rust-based data structures for circuits. This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well. This crate is a very low part of the Rust stack, if not the very lowest. + +The data model exposed by this crate is as follows. + +## CircuitData + +The core representation of a quantum circuit in Rust is the `CircuitData` struct. This containts the list +of instructions that are comprising the circuit. Each element in this list is modeled by a +`CircuitInstruction` struct. The `CircuitInstruction` contains the operation object and it's operands. +This includes the parameters and bits. It also contains the potential mutable state of the Operation representation from the legacy Python data model; namely `duration`, `unit`, `condition`, and `label`. +In the future we'll be able to remove all of that except for label. + +At rest a `CircuitInstruction` is compacted into a `PackedInstruction` which caches reused qargs +in the instructions to reduce the memory overhead of `CircuitData`. The `PackedInstruction` objects +get unpacked back to `CircuitInstruction` when accessed for a more convienent working form. + +Additionally the `CircuitData` contains a `param_table` field which is used to track parameterized +instructions that are using python defined `ParameterExpression` objects for any parameters and also +a global phase field which is used to track the global phase of the circuit. + +## Operation Model + +In the circuit crate all the operations used in a `CircuitInstruction` are part of the `OperationType` +enum. The `OperationType` enum has four variants which are used to define the different types of +operation objects that can be on a circuit: + + - `StandardGate`: a rust native representation of a member of the Qiskit standard gate library. This is + an `enum` that enumerates all the gates in the library and statically defines all the gate properties + except for gates that take parameters, + - `PyGate`: A struct that wraps a gate outside the standard library defined in Python. This struct wraps + a `Gate` instance (or subclass) as a `PyObject`. The static properties of this object (such as name, + number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the matrix or definition are accessed by calling back into Python to get them from the stored + `PyObject` + - `PyInstruction`: A struct that wraps an instruction defined in Python. This struct wraps an + `Instruction` instance (or subclass) as a `PyObject`. The static properties of this object (such as + name, number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the definition are accessed by calling back into Python to get them from the stored `PyObject`. As + the primary difference between `Gate` and `Instruction` in the python data model are that `Gate` is a + specialized `Instruction` subclass that represents unitary operations the primary difference between + this and `PyGate` are that `PyInstruction` will always return `None` when it's matrix is accessed. + - `PyOperation`: A struct that wraps an operation defined in Python. This struct wraps an `Operation` + instance (or subclass) as a `PyObject`. The static properties of this object (such as name, number + of qubits, etc) are stored in Rust for performance. As `Operation` is the base abstract interface + definition of what can be put on a circuit this is mostly just a container for custom Python objects. + Anything that's operating on a bare operation will likely need to access it via the `PyObject` + manually because the interface doesn't define many standard properties outside of what's cached in + the struct. + +There is also an `Operation` trait defined which defines the common access pattern interface to these +4 types along with the `OperationType` parent. This trait defines methods to access the standard data +model attributes of operations in Qiskit. This includes things like the name, number of qubits, the matrix, the definition, etc. + +## ParamTable + +The `ParamTable` struct is used to track which circuit instructions are using `ParameterExpression` +objects for any of their parameters. The Python space `ParameterExpression` is comprised of a symengine +symbolic expression that defines operations using `Parameter` objects. Each `Parameter` is modeled by +a uuid and a name to uniquely identify it. The parameter table maps the `Parameter` objects to the +`CircuitInstruction` in the `CircuitData` that are using them. The `Parameter` comprised of 3 `HashMaps` internally that map the uuid (as `u128`, which is accesible in Python by using `uuid.int`) to the `ParamEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. + +The `ParamEntry` is just a `HashSet` of 2-tuples with usize elements. The two usizes represent the instruction index in the `CircuitData` and the index of the `CircuitInstruction.params` field of +a give instruction where the given `Parameter` is used in the circuit. If the instruction index is +`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`. diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 7964ec186e0..40540f9df5a 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -12,7 +12,7 @@ use crate::BitType; use hashbrown::HashMap; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyList; use std::fmt::Debug; @@ -83,6 +83,15 @@ pub(crate) struct BitData { pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); +impl<'py> From> for PyErr { + fn from(error: BitNotFoundError) -> Self { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + error.0 + )) + } +} + impl BitData where T: From + Copy, @@ -142,7 +151,7 @@ where /// Map the provided native indices to the corresponding Python /// bit instances. /// Panics if any of the indices are out of range. - pub fn map_indices(&self, bits: &[T]) -> impl Iterator> + ExactSizeIterator { + pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator> { let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect(); v.into_iter() } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index fbb7c06fc89..da35787e320 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,17 +10,24 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::bit_data::{BitData, BitNotFoundError}; -use crate::circuit_instruction::CircuitInstruction; -use crate::interner::{CacheFullError, IndexedInterner, Interner, InternerKey}; -use crate::packed_instruction::PackedInstruction; +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, + ExtraInstructionAttributes, OperationInput, PackedInstruction, +}; +use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::operations::{Operation, OperationType, Param, StandardGate}; +use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; use crate::{Clbit, Qubit, SliceOrInt}; -use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; -use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; -use std::mem; +use pyo3::{intern, PyTraverseError, PyVisit}; + +use hashbrown::{HashMap, HashSet}; +use smallvec::SmallVec; /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning @@ -85,33 +92,250 @@ pub struct CircuitData { qubits: BitData, /// Clbits registered in the circuit. clbits: BitData, + param_table: ParamTable, + #[pyo3(get)] + global_phase: Param, } -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) +impl CircuitData { + /// An alternate constructor to build a new `CircuitData` from an iterator + /// of standard gates. This can be used to build a circuit from a sequence + /// of standard gates, such as for a `StandardGate` definition or circuit + /// synthesis without needing to involve Python. + /// + /// This can be connected with the Python space + /// QuantumCircuit.from_circuit_data() constructor to build a full + /// QuantumCircuit from Rust. + /// + /// # Arguments + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * num_qubits: The number of qubits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * instructions: An iterator of the standard gate params and qubits to + /// add to the circuit + /// * global_phase: The global phase to use for the circuit + pub fn from_standard_gates( + py: Python, + num_qubits: u32, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator, SmallVec<[Qubit; 2]>)>, + { + let instruction_iter = instructions.into_iter(); + let mut res = CircuitData { + data: Vec::with_capacity(instruction_iter.size_hint().0), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_cls = QUBIT.get_bound(py); + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + for (operation, params, qargs) in instruction_iter { + let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); + let clbits = PyTuple::empty_bound(py).unbind(); + let inst = res.pack_owned( + py, + &CircuitInstruction { + operation: OperationType::Standard(operation), + qubits, + clbits, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: None, + }, + )?; + res.data.push(inst); + } + Ok(res) } -} -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") + fn handle_manual_params( + &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)?; + } + }; + } + atomic_parameters.clear() + } + Ok(new_param) + } + + /// Add an instruction's entries to the parameter table + fn update_param_table( + &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; + 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.is_empty() { + let params: Vec<(usize, PyObject)> = self.data[inst_index] + .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() { + 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); + } + } + } + } + Ok(()) + } + + 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)?; + } + // 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: PyRef) -> 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] impl CircuitData { #[new] - #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))] + #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))] pub fn new( py: Python<'_>, qubits: Option<&Bound>, clbits: Option<&Bound>, data: Option<&Bound>, reserve: usize, + global_phase: Param, ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), @@ -119,7 +343,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(), + global_phase: Param::Float(0.), }; + self_.global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { self_.add_qubit(py, &bit?, true)?; @@ -241,17 +468,89 @@ impl CircuitData { /// /// Returns: /// CircuitData: The shallow copy. - pub fn copy(&self, py: Python<'_>) -> PyResult { + #[pyo3(signature = (copy_instructions=true, deepcopy=false))] + pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { let mut res = CircuitData::new( py, Some(self.qubits.cached().bind(py)), Some(self.clbits.cached().bind(py)), None, 0, + self.global_phase.clone(), )?; res.qargs_interner = self.qargs_interner.clone(); res.cargs_interner = self.cargs_interner.clone(); res.data.clone_from(&self.data); + res.param_table.clone_from(&self.param_table); + + if deepcopy { + let deepcopy = py + .import_bound(intern!(py, "copy"))? + .getattr(intern!(py, "deepcopy"))?; + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = deepcopy.call1((&op.gate,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = deepcopy.call1((&op.operation,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } else if copy_instructions { + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } Ok(res) } @@ -290,10 +589,87 @@ impl CircuitData { /// Args: /// func (Callable[[:class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - func.call1((inst.op.bind(py),))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((op,))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((op,))?; } Ok(()) } @@ -304,10 +680,88 @@ impl CircuitData { /// Args: /// func (Callable[[int, :class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - func.call1((index, inst.op.bind(py)))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((index, op))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with the positional index and operation + /// of each instruction. + /// + /// Args: + /// func (Callable[[int, :class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op_indexed(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for (index, inst) in self.data.iter_mut().enumerate() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((index, op))?; } Ok(()) } @@ -315,14 +769,187 @@ impl CircuitData { /// Invokes callable ``func`` with each instruction's operation, /// replacing the operation with the result. /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// /// Args: /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their /// replacements. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - inst.op = func.call1((inst.op.bind(py),))?.into_py(py); + let old_op = match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )? + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op)?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + } + } + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation, + /// replacing the operation with the result. + /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): + /// A callable used to map original operation to their + /// replacements. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let old_op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }, + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + inst.py_op = Some(new_op); + } + } } Ok(()) } @@ -385,7 +1012,7 @@ impl CircuitData { qubits: Option<&Bound>, clbits: Option<&Bound>, ) -> PyResult<()> { - let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; + let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?; if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -394,7 +1021,7 @@ impl CircuitData { self.num_qubits(), ))); } - mem::swap(&mut temp.qubits, &mut self.qubits); + std::mem::swap(&mut temp.qubits, &mut self.qubits); } if clbits.is_some() { if temp.num_clbits() < self.num_clbits() { @@ -404,7 +1031,7 @@ impl CircuitData { self.num_clbits(), ))); } - mem::swap(&mut temp.clbits, &mut self.clbits); + std::mem::swap(&mut temp.clbits, &mut self.clbits); } Ok(()) } @@ -430,9 +1057,11 @@ impl CircuitData { py, CircuitInstruction::new( py, - inst.op.clone_ref(py), + inst.op.clone(), self_.qubits.map_indices(qubits.value), self_.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), ), ) } else { @@ -455,7 +1084,7 @@ impl CircuitData { } } - pub fn __delitem__(&mut self, index: SliceOrInt) -> PyResult<()> { + pub fn __delitem__(&mut self, py: Python, index: SliceOrInt) -> PyResult<()> { match index { SliceOrInt::Slice(slice) => { let slice = { @@ -468,14 +1097,24 @@ impl CircuitData { s }; for i in slice.into_iter() { - self.__delitem__(SliceOrInt::Int(i))?; + self.__delitem__(py, SliceOrInt::Int(i))?; } + self.reindex_parameter_table(py)?; Ok(()) } SliceOrInt::Int(index) => { let index = self.convert_py_index(index)?; if self.data.get(index).is_some() { - self.data.remove(index); + if index == self.data.len() { + // For individual removal from param table before + // deletion + self.remove_from_parameter_table(py, index)?; + self.data.remove(index); + } else { + // For delete in the middle delete before reindexing + self.data.remove(index); + self.reindex_parameter_table(py)?; + } Ok(()) } else { Err(PyIndexError::new_err(format!( @@ -487,6 +1126,19 @@ impl CircuitData { } } + pub fn setitem_no_param_table_update( + &mut self, + py: Python<'_>, + index: isize, + value: &Bound, + ) -> PyResult<()> { + let index = self.convert_py_index(index)?; + let value: PyRef = value.downcast()?.borrow(); + let mut packed = self.pack(py, value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } + pub fn __setitem__( &mut self, py: Python<'_>, @@ -520,7 +1172,7 @@ impl CircuitData { indices.stop, 1isize, ); - self.__delitem__(SliceOrInt::Slice(slice))?; + self.__delitem__(py, SliceOrInt::Slice(slice))?; } else { // Insert any extra values. for v in values.iter().skip(slice.len()).rev() { @@ -535,7 +1187,9 @@ impl CircuitData { let index = self.convert_py_index(index)?; let value: PyRef = value.extract()?; let mut packed = self.pack(py, value)?; - mem::swap(&mut packed, &mut self.data[index]); + self.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut self.data[index]); + self.update_param_table(py, index, None)?; Ok(()) } } @@ -548,8 +1202,14 @@ impl CircuitData { value: PyRef, ) -> PyResult<()> { let index = self.convert_py_index_clamped(index); + let old_len = self.data.len(); let packed = self.pack(py, value)?; self.data.insert(index, packed); + if index == old_len { + self.update_param_table(py, old_len, None)?; + } else { + self.reindex_parameter_table(py)?; + } Ok(()) } @@ -557,14 +1217,21 @@ impl CircuitData { let index = index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); let item = self.__getitem__(py, index.bind(py))?; - self.__delitem__(index.bind(py).extract()?)?; + + self.__delitem__(py, index.bind(py).extract()?)?; Ok(item) } - pub fn append(&mut self, py: Python<'_>, value: PyRef) -> PyResult<()> { - let packed = self.pack(py, value)?; + pub fn append( + &mut self, + py: Python<'_>, + value: &Bound, + params: Option)>>, + ) -> PyResult { + let packed = self.pack(py, value.try_borrow()?)?; + let new_index = self.data.len(); self.data.push(packed); - Ok(()) + self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { @@ -597,28 +1264,33 @@ impl CircuitData { .unwrap()) }) .collect::>>()?; - + let new_index = self.data.len(); let qubits_id = Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; let clbits_id = Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; self.data.push(PackedInstruction { - op: inst.op.clone_ref(py), + op: inst.op.clone(), qubits_id: qubits_id.index, clbits_id: clbits_id.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }); + self.update_param_table(py, new_index, None)?; } return Ok(()); } - for v in itr.iter()? { - self.append(py, v?.extract()?)?; + self.append_inner(py, v?.extract()?)?; } Ok(()) } pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { std::mem::take(&mut self.data); + self.param_table.clear(); Ok(()) } @@ -656,9 +1328,6 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for packed in self.data.iter() { - visit.call(&packed.op)?; - } for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { visit.call(bit)?; } @@ -678,6 +1347,128 @@ impl CircuitData { self.qubits.dispose(); self.clbits.dispose(); } + + #[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)?; + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + } + 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)?; + } + }; + } + 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: String, + 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); + Ok(()) + } + None => Err(PyIndexError::new_err(format!( + "Invalid parameter uuid: {:?}", + uuid + ))), + } + } + + 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() + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) + .count() + } } impl CircuitData { @@ -730,23 +1521,43 @@ impl CircuitData { Ok(index as usize) } - fn pack( - &mut self, - py: Python, - value: PyRef, - ) -> PyResult { + fn pack(&mut self, py: Python, inst: PyRef) -> PyResult { + let qubits = Interner::intern( + &mut self.qargs_interner, + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), + )?; + let clbits = Interner::intern( + &mut self.cargs_interner, + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), + )?; + Ok(PackedInstruction { + op: inst.operation.clone(), + qubits_id: qubits.index, + clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), + }) + } + + fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(value.qubits.bind(py))?.collect()), + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), )?; let clbits = Interner::intern( &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(value.clbits.bind(py))?.collect()), + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), )?; Ok(PackedInstruction { - op: value.operation.clone_ref(py), + op: inst.operation.clone(), qubits_id: qubits.index, clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ac61ae81a61..2bb90367082 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -11,9 +11,45 @@ // that they have been altered from the originals. use pyo3::basic::CompareOp; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyList, PyTuple}; -use pyo3::{PyObject, PyResult}; +use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; +use pyo3::{intern, IntoPy, PyObject, PyResult}; +use smallvec::{smallvec, SmallVec}; + +use crate::imports::{ + get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, +}; +use crate::interner::Index; +use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; + +/// These are extra mutable attributes for a circuit instruction's state. In general we don't +/// typically deal with this in rust space and the majority of the time they're not used in Python +/// space either. To save memory these are put in a separate struct and are stored inside a +/// `Box` on `CircuitInstruction` and `PackedInstruction`. +#[derive(Debug, Clone)] +pub struct ExtraInstructionAttributes { + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Private type used to store instructions with interned arg lists. +#[derive(Clone, Debug)] +pub(crate) struct PackedInstruction { + /// The Python-side operation instance. + pub op: OperationType, + /// The index under which the interner has stored `qubits`. + pub qubits_id: Index, + /// The index under which the interner has stored `clbits`. + pub clbits_id: Index, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. @@ -47,28 +83,45 @@ use pyo3::{PyObject, PyResult}; /// mutations of the object do not invalidate the types, nor the restrictions placed on it by /// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence /// of distinct items, with no duplicates. -#[pyclass( - freelist = 20, - sequence, - get_all, - module = "qiskit._accelerate.circuit" -)] +#[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")] #[derive(Clone, Debug)] pub struct CircuitInstruction { - /// The logical operation that this instruction represents an execution of. - pub operation: PyObject, + pub operation: OperationType, /// A sequence of the qubits that the operation is applied to. + #[pyo3(get)] pub qubits: Py, /// A sequence of the classical bits that this operation reads from or writes to. + #[pyo3(get)] pub clbits: Py, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} + +/// This enum is for backwards compatibility if a user was doing something from +/// Python like CircuitInstruction(SXGate(), [qr[0]], []) by passing a python +/// gate object directly to a CircuitInstruction. In this case we need to +/// create a rust side object from the pyobject in CircuitInstruction.new() +/// With the `Object` variant which will convert the python object to a rust +/// `OperationType` +#[derive(FromPyObject, Debug)] +pub enum OperationInput { + Standard(StandardGate), + Gate(PyGate), + Instruction(PyInstruction), + Operation(PyOperation), + Object(PyObject), } impl CircuitInstruction { pub fn new( py: Python, - operation: PyObject, + operation: OperationType, qubits: impl IntoIterator, clbits: impl IntoIterator, + params: SmallVec<[Param; 3]>, + extra_attrs: Option>, ) -> Self where T1: ToPyObject, @@ -80,19 +133,41 @@ impl CircuitInstruction { operation, qubits: PyTuple::new_bound(py, qubits).unbind(), clbits: PyTuple::new_bound(py, clbits).unbind(), + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + } + } +} + +impl From for OperationInput { + fn from(value: OperationType) -> Self { + match value { + OperationType::Standard(op) => Self::Standard(op), + OperationType::Gate(gate) => Self::Gate(gate), + OperationType::Instruction(inst) => Self::Instruction(inst), + OperationType::Operation(op) => Self::Operation(op), } } } #[pymethods] impl CircuitInstruction { + #[allow(clippy::too_many_arguments)] #[new] + #[pyo3(signature = (operation, qubits=None, clbits=None, params=smallvec![], label=None, duration=None, unit=None, condition=None))] pub fn py_new( py: Python<'_>, - operation: PyObject, + operation: OperationInput, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult> { + params: SmallVec<[Param; 3]>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { match seq { None => Ok(PyTuple::empty_bound(py).unbind()), @@ -116,14 +191,136 @@ impl CircuitInstruction { } } - Py::new( - py, - CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - }, - ) + let extra_attrs = + if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { + Some(Box::new(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + })) + } else { + None + }; + + match operation { + OperationInput::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Object(old_op) => { + let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?; + let extra_attrs = if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })) + } else { + None + }; + + match op.operation { + OperationType::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + } + } + } } /// Returns a shallow copy. @@ -134,28 +331,127 @@ impl CircuitInstruction { self.clone() } + /// The logical operation that this instruction represents an execution of. + #[cfg(not(feature = "cache_pygates"))] + #[getter] + pub fn operation(&self, py: Python) -> PyResult { + operation_type_to_py(py, self) + } + + #[cfg(feature = "cache_pygates")] + #[getter] + pub fn operation(&mut self, py: Python) -> PyResult { + Ok(match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: /// CircuitInstruction: A new instance with the given fields replaced. + #[allow(clippy::too_many_arguments)] pub fn replace( &self, py: Python<'_>, - operation: Option, + operation: Option, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult> { + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { + let operation = operation.unwrap_or_else(|| self.operation.clone().into()); + + let params = match params { + Some(params) => params, + None => self.params.clone(), + }; + + let label = match label { + Some(label) => Some(label), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.label.clone(), + None => None, + }, + }; + let duration = match duration { + Some(duration) => Some(duration), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.duration.clone(), + None => None, + }, + }; + + let unit: Option = match unit { + Some(unit) => Some(unit), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.unit.clone(), + None => None, + }, + }; + + let condition: Option = match condition { + Some(condition) => Some(condition), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.condition.clone(), + None => None, + }, + }; + CircuitInstruction::py_new( py, - operation.unwrap_or_else(|| self.operation.clone_ref(py)), + operation, Some(qubits.unwrap_or_else(|| self.qubits.bind(py))), Some(clbits.unwrap_or_else(|| self.clbits.bind(py))), + params, + label, + duration, + unit, + condition, ) } - fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + fn __getstate__(&self, py: Python<'_>) -> PyResult { Ok(( - self.operation.bind(py), + operation_type_to_py(py, self)?, + self.qubits.bind(py), + self.clbits.bind(py), + ) + .into_py(py)) + } + + fn __setstate__(&mut self, py: Python<'_>, state: &Bound) -> PyResult<()> { + let op = convert_py_to_operation_type(py, state.get_item(0)?.into())?; + self.operation = op.operation; + self.params = op.params; + self.qubits = state.get_item(1)?.extract()?; + self.clbits = state.get_item(2)?.extract()?; + if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + self.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })); + } + Ok(()) + } + + pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + Ok(( + operation_type_to_py(py, self)?, self.qubits.bind(py), self.clbits.bind(py), ) @@ -172,7 +468,7 @@ impl CircuitInstruction { , clbits={}\ )", type_name, - r.operation.bind(py).repr()?, + operation_type_to_py(py, &r)?, r.qubits.bind(py).repr()?, r.clbits.bind(py).repr()? )) @@ -184,23 +480,50 @@ impl CircuitInstruction { // the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated // like that via unpacking or similar. That means that the `parameters` field is completely // absent, and the qubits and clbits must be converted to lists. - pub fn _legacy_format<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + #[cfg(not(feature = "cache_pygates"))] + pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult> { + let op = operation_type_to_py(py, self)?; + + Ok(PyTuple::new_bound( py, - [ - self.operation.bind(py), - &self.qubits.bind(py).to_list(), - &self.clbits.bind(py).to_list(), - ], - ) + [op, self.qubits.to_object(py), self.clbits.to_object(py)], + )) } + #[cfg(feature = "cache_pygates")] + pub fn _legacy_format<'py>(&mut self, py: Python<'py>) -> PyResult> { + let op = match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }; + Ok(PyTuple::new_bound( + py, + [op, self.qubits.to_object(py), self.clbits.to_object(py)], + )) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { - Ok(self._legacy_format(py).as_any().get_item(key)?.into_py(py)) + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } + #[cfg(feature = "cache_pygates")] + pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __iter__(&self, py: Python<'_>) -> PyResult { - Ok(self._legacy_format(py).as_any().iter()?.into_py(py)) + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) + } + + #[cfg(feature = "cache_pygates")] + pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } pub fn __len__(&self) -> usize { @@ -227,16 +550,94 @@ impl CircuitInstruction { let other: PyResult> = other.extract(); return other.map_or(Ok(Some(false)), |v| { let v = v.try_borrow()?; + let op_eq = match &self_.operation { + OperationType::Standard(op) => { + if let OperationType::Standard(other) = &v.operation { + if op != other { + false + } else { + let other_params = &v.params; + let mut out = true; + for (param_a, param_b) in self_.params.iter().zip(other_params) + { + match param_a { + Param::Float(val_a) => { + if let Param::Float(val_b) = param_b { + if val_a != val_b { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::ParameterExpression(val_a) => { + if let Param::ParameterExpression(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::Obj(val_a) => { + if let Param::Obj(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + } + } + out + } + } else { + false + } + } + OperationType::Gate(op) => { + if let OperationType::Gate(other) = &v.operation { + op.gate.bind(py).eq(other.gate.bind(py))? + } else { + false + } + } + OperationType::Instruction(op) => { + if let OperationType::Instruction(other) = &v.operation { + op.instruction.bind(py).eq(other.instruction.bind(py))? + } else { + false + } + } + OperationType::Operation(op) => { + if let OperationType::Operation(other) = &v.operation { + op.operation.bind(py).eq(other.operation.bind(py))? + } else { + false + } + } + }; + Ok(Some( self_.clbits.bind(py).eq(v.clbits.bind(py))? && self_.qubits.bind(py).eq(v.qubits.bind(py))? - && self_.operation.bind(py).eq(v.operation.bind(py))?, + && op_eq, )) }); } if other.is_instance_of::() { - return Ok(Some(self_._legacy_format(py).eq(other)?)); + #[cfg(feature = "cache_pygates")] + let mut self_ = self_.clone(); + let legacy_format = self_._legacy_format(py)?; + return Ok(Some(legacy_format.eq(other)?)); } Ok(None) @@ -255,3 +656,222 @@ impl CircuitInstruction { } } } + +/// Take a reference to a `CircuitInstruction` and convert the operation +/// inside that to a python side object. +pub(crate) fn operation_type_to_py( + py: Python, + circuit_inst: &CircuitInstruction, +) -> PyResult { + let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { + None => (None, None, None, None), + Some(extra_attrs) => ( + extra_attrs.label.clone(), + extra_attrs.duration.clone(), + extra_attrs.unit.clone(), + extra_attrs.condition.clone(), + ), + }; + operation_type_and_data_to_py( + py, + &circuit_inst.operation, + &circuit_inst.params, + &label, + &duration, + &unit, + &condition, + ) +} + +/// Take an OperationType and the other mutable state fields from a +/// rust instruction representation and return a PyObject representing +/// a Python side full-fat Qiskit operation as a PyObject. This is typically +/// used by accessor functions that need to return an operation to Qiskit, such +/// as accesing `CircuitInstruction.operation`. +pub(crate) fn operation_type_and_data_to_py( + py: Python, + operation: &OperationType, + params: &[Param], + label: &Option, + duration: &Option, + unit: &Option, + condition: &Option, +) -> PyResult { + match &operation { + OperationType::Standard(op) => { + let gate_class: &PyObject = &get_std_gate_class(py, *op)?; + + let args = if params.is_empty() { + PyTuple::empty_bound(py) + } else { + PyTuple::new_bound(py, params) + }; + let kwargs = [ + ("label", label.to_object(py)), + ("unit", unit.to_object(py)), + ("duration", duration.to_object(py)), + ] + .into_py_dict_bound(py); + let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; + if condition.is_some() { + out = out.call_method0(py, "to_mutable")?; + out.setattr(py, "condition", condition.to_object(py))?; + } + Ok(out) + } + OperationType::Gate(gate) => Ok(gate.gate.clone_ref(py)), + OperationType::Instruction(inst) => Ok(inst.instruction.clone_ref(py)), + OperationType::Operation(op) => Ok(op.operation.clone_ref(py)), + } +} + +/// A container struct that contains the output from the Python object to +/// conversion to construct a CircuitInstruction object +#[derive(Debug)] +pub(crate) struct OperationTypeConstruct { + pub operation: OperationType, + pub params: SmallVec<[Param; 3]>, + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Convert an inbound Python object for a Qiskit operation and build a rust +/// representation of that operation. This will map it to appropriate variant +/// of operation type based on class +pub(crate) fn convert_py_to_operation_type( + py: Python, + py_op: PyObject, +) -> PyResult { + let attr = intern!(py, "_standard_gate"); + let py_op_bound = py_op.clone_ref(py).into_bound(py); + // Get PyType from either base_class if it exists, or if not use the + // class/type info from the pyobject + let binding = py_op_bound.getattr(intern!(py, "base_class")).ok(); + let op_obj = py_op_bound.get_type(); + let raw_op_type: Py = match binding { + Some(base_class) => base_class.downcast()?.clone().unbind(), + None => op_obj.unbind(), + }; + let op_type: Bound = raw_op_type.into_bound(py); + let mut standard: Option = match op_type.getattr(attr) { + Ok(stdgate) => match stdgate.extract().ok() { + Some(gate) => gate, + None => None, + }, + Err(_) => None, + }; + // If the input instruction is a standard gate and a singleton instance + // we should check for mutable state. A mutable instance should be treated + // as a custom gate not a standard gate because it has custom properties. + // + // In the futuer we can revisit this when we've dropped `duration`, `unit`, + // and `condition` from the api as we should own the label in the + // `CircuitInstruction`. The other piece here is for controlled gates there + // is the control state, so for `SingletonControlledGates` we'll still need + // this check. + if standard.is_some() { + let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; + if mutable + && (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))? + || py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?) + { + standard = None; + } + } + if let Some(op) = standard { + let base_class = op_type.to_object(py); + populate_std_gate_map(py, op, base_class); + return Ok(OperationTypeConstruct { + operation: OperationType::Standard(op), + params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?, + label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?, + duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?, + unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?, + condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, + }); + } + if op_type.is_subclass(GATE.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyGate { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + gate: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Gate(out_op), + params, + label, + duration, + unit, + condition, + }); + } + if op_type.is_subclass(INSTRUCTION.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyInstruction { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + instruction: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Instruction(out_op), + params, + label, + duration, + unit, + condition, + }); + } + + if op_type.is_subclass(OPERATION.get_bound(py))? { + let params = match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.extract(py)?, + Err(_) => smallvec![], + }; + let label = None; + let duration = None; + let unit = None; + let condition = None; + let out_op = PyOperation { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.downcast_bound::(py)?.len() as u32, + Err(_) => 0, + }, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + operation: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Operation(out_op), + params, + label, + duration, + unit, + condition, + }); + } + Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) +} diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c766461bb51..c8b6a4c8b08 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -10,7 +10,11 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::circuit_instruction::CircuitInstruction; +use crate::circuit_instruction::{ + convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, + ExtraInstructionAttributes, +}; +use crate::operations::Operation; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; use pyo3::{intern, PyObject, PyResult}; @@ -106,13 +110,33 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; + let res = convert_py_to_operation_type(py, op.clone_ref(py))?; + + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; Ok(( DAGOpNode { instruction: CircuitInstruction { - operation: op, + operation: res.operation, qubits: qargs.unbind(), clbits: cargs.unbind(), + params: res.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(op), }, sort_key: sort_key.unbind(), }, @@ -120,18 +144,18 @@ impl DAGOpNode { )) } - fn __reduce__(slf: PyRef, py: Python) -> PyObject { + fn __reduce__(slf: PyRef, py: Python) -> PyResult { let state = (slf.as_ref()._node_id, &slf.sort_key); - ( + Ok(( py.get_type_bound::(), ( - &slf.instruction.operation, + operation_type_to_py(py, &slf.instruction)?, &slf.instruction.qubits, &slf.instruction.clbits, ), state, ) - .into_py(py) + .into_py(py)) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { @@ -142,13 +166,31 @@ impl DAGOpNode { } #[getter] - fn get_op(&self, py: Python) -> PyObject { - self.instruction.operation.clone_ref(py) + fn get_op(&self, py: Python) -> PyResult { + operation_type_to_py(py, &self.instruction) } #[setter] - fn set_op(&mut self, op: PyObject) { - self.instruction.operation = op; + fn set_op(&mut self, py: Python, op: PyObject) -> PyResult<()> { + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + self.instruction.params = res.params; + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; + self.instruction.extra_attrs = extra_attrs; + Ok(()) } #[getter] @@ -173,29 +215,27 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> PyResult { - Ok(self - .instruction - .operation - .bind(py) - .getattr(intern!(py, "name"))? - .unbind()) + fn get_name(&self, py: Python) -> PyObject { + self.instruction.operation.name().to_object(py) } /// Sets the Instruction name corresponding to the op for this node #[setter] - fn set_name(&self, py: Python, new_name: PyObject) -> PyResult<()> { - self.instruction - .operation - .bind(py) - .setattr(intern!(py, "name"), new_name) + fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { + let op = operation_type_to_py(py, &self.instruction)?; + op.bind(py).setattr(intern!(py, "name"), new_name)?; + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + Ok(()) } /// Returns a representation of the DAGOpNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!( "DAGOpNode(op={}, qargs={}, cargs={})", - self.instruction.operation.bind(py).repr()?, + operation_type_to_py(py, &self.instruction)? + .bind(py) + .repr()?, self.instruction.qubits.bind(py).repr()?, self.instruction.clbits.bind(py).repr()? )) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs new file mode 100644 index 00000000000..72e1087637c --- /dev/null +++ b/crates/circuit/src/gate_matrix.rs @@ -0,0 +1,224 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +// num-complex exposes an equivalent function but it's not a const function +// so it's not compatible with static definitions. This is a const func and +// just reduces the amount of typing we need. +#[inline(always)] +const fn c64(re: f64, im: f64) -> Complex64 { + Complex64::new(re, im) +} + +pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = + [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(1., 0.)]]; + +#[inline] +pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., -half_theta.sin()); + [[cos, isin], [isin, cos]] +} + +#[inline] +pub fn ry_gate(theta: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [[cos, -sin], [sin, cos]] +} + +#[inline] +pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] { + let ilam2 = c64(0., 0.5 * theta); + [[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]] +} + +pub static H_GATE: [[Complex64; 2]; 2] = [ + [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], + [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], +]; + +pub static CX_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], +]; + +pub static SX_GATE: [[Complex64; 2]; 2] = [ + [c64(0.5, 0.5), c64(0.5, -0.5)], + [c64(0.5, -0.5), c64(0.5, 0.5)], +]; + +pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; + +pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; + +pub static Y_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; + +pub static CZ_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(-1., 0.)], +]; + +pub static CY_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(0., -1.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)], +]; + +pub static CCX_GATE: [[Complex64; 8]; 8] = [ + [ + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], +]; + +pub static ECR_GATE: [[Complex64; 4]; 4] = [ + [ + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), + ], + [ + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + ], + [ + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + ], +]; + +pub static SWAP_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], +]; + +#[inline] +pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { + [[c64(0., theta).exp()]] +} + +#[inline] +pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] { + [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., lam).exp()], + ] +} + +#[inline] +pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), (-c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs new file mode 100644 index 00000000000..050f7f2e053 --- /dev/null +++ b/crates/circuit/src/imports.rs @@ -0,0 +1,168 @@ +// 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. + +// This module contains objects imported from Python that are reused. These are +// typically data model classes that are used to identify an object, or for +// python side casting + +use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; + +use crate::operations::{StandardGate, STANDARD_GATE_SIZE}; + +/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object +/// that is lazily imported. +pub struct ImportOnceCell { + module: &'static str, + object: &'static str, + cell: GILOnceCell>, +} + +impl ImportOnceCell { + const fn new(module: &'static str, object: &'static str) -> Self { + Self { + module, + object, + cell: GILOnceCell::new(), + } + } + + /// Get the underlying GIL-independent reference to the contained object, importing if + /// required. + #[inline] + pub fn get(&self, py: Python) -> &Py { + self.cell.get_or_init(py, || { + py.import_bound(self.module) + .unwrap() + .getattr(self.object) + .unwrap() + .unbind() + }) + } + + /// Get a GIL-bound reference to the contained object, importing if required. + #[inline] + pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> { + self.get(py).bind(py) + } +} + +pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); +pub static INSTRUCTION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); +pub static GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.gate", "Gate"); +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 = + ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); +pub static QUANTUM_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); +pub static SINGLETON_GATE: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); +pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); + +/// 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 +/// when a gate is added directly via the StandardGate path and there isn't a Python object +/// to poll the _standard_gate attribute for. +/// +/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ + // ZGate = 0 + ["qiskit.circuit.library.standard_gates.z", "ZGate"], + // YGate = 1 + ["qiskit.circuit.library.standard_gates.y", "YGate"], + // XGate = 2 + ["qiskit.circuit.library.standard_gates.x", "XGate"], + // CZGate = 3 + ["qiskit.circuit.library.standard_gates.z", "CZGate"], + // CYGate = 4 + ["qiskit.circuit.library.standard_gates.y", "CYGate"], + // CXGate = 5 + ["qiskit.circuit.library.standard_gates.x", "CXGate"], + // CCXGate = 6 + ["qiskit.circuit.library.standard_gates.x", "CCXGate"], + // RXGate = 7 + ["qiskit.circuit.library.standard_gates.rx", "RXGate"], + // RYGate = 8 + ["qiskit.circuit.library.standard_gates.ry", "RYGate"], + // RZGate = 9 + ["qiskit.circuit.library.standard_gates.rz", "RZGate"], + // ECRGate = 10 + ["qiskit.circuit.library.standard_gates.ecr", "ECRGate"], + // SwapGate = 11 + ["qiskit.circuit.library.standard_gates.swap", "SwapGate"], + // SXGate = 12 + ["qiskit.circuit.library.standard_gates.sx", "SXGate"], + // GlobalPhaseGate = 13 + [ + "qiskit.circuit.library.standard_gates.global_phase", + "GlobalPhaseGate", + ], + // IGate = 14 + ["qiskit.circuit.library.standard_gates.i", "IGate"], + // HGate = 15 + ["qiskit.circuit.library.standard_gates.h", "HGate"], + // PhaseGate = 16 + ["qiskit.circuit.library.standard_gates.p", "PhaseGate"], + // UGate = 17 + ["qiskit.circuit.library.standard_gates.u", "UGate"], +]; + +/// A mapping from the enum variant in crate::operations::StandardGate to the python object for the +/// class that matches it. This is typically used when we need to convert from the internal rust +/// representation to a Python object for a python user to interact with. +/// +/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +static mut STDGATE_PYTHON_GATES: GILOnceCell<[Option; STANDARD_GATE_SIZE]> = + GILOnceCell::new(); + +#[inline] +pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObject) { + let gate_map = unsafe { + match STDGATE_PYTHON_GATES.get_mut() { + Some(gate_map) => gate_map, + None => { + let array: [Option; STANDARD_GATE_SIZE] = std::array::from_fn(|_| None); + STDGATE_PYTHON_GATES.set(py, array).unwrap(); + STDGATE_PYTHON_GATES.get_mut().unwrap() + } + } + }; + let gate_cls = &gate_map[rs_gate as usize]; + if gate_cls.is_none() { + gate_map[rs_gate as usize] = Some(py_gate.clone_ref(py)); + } +} + +#[inline] +pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { + let gate_map = + unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || std::array::from_fn(|_| None)) }; + let gate = &gate_map[rs_gate as usize]; + let populate = gate.is_none(); + let out_gate = match gate { + Some(gate) => gate.clone_ref(py), + None => { + let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize]; + py.import_bound(py_mod)?.getattr(py_class)?.unbind() + } + }; + if populate { + populate_std_gate_map(py, rs_gate, out_gate.clone_ref(py)); + } + Ok(out_gate) +} diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index 42667570205..f22bb80ae05 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -10,11 +10,13 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use hashbrown::HashMap; -use pyo3::{IntoPy, PyObject, Python}; use std::hash::Hash; use std::sync::Arc; +use hashbrown::HashMap; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; + #[derive(Clone, Copy, Debug)] pub struct Index(u32); @@ -42,6 +44,12 @@ impl IntoPy for Index { pub struct CacheFullError; +impl From for PyErr { + fn from(_: CacheFullError) -> Self { + PyRuntimeError::new_err("The bit operands cache is full!") + } +} + /// An append-only data structure for interning generic /// Rust types. #[derive(Clone, Debug)] diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 90f2b7c7f07..d7f28591175 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,10 +13,13 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; +pub mod gate_matrix; +pub mod imports; +pub mod operations; +pub mod parameter_table; mod bit_data; mod interner; -mod packed_instruction; use pyo3::prelude::*; use pyo3::types::PySlice; @@ -33,9 +36,9 @@ pub enum SliceOrInt<'a> { pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] -pub struct Qubit(BitType); +pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] -pub struct Clbit(BitType); +pub struct Clbit(pub BitType); impl From for Qubit { fn from(value: BitType) -> Self { @@ -69,5 +72,9 @@ pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs new file mode 100644 index 00000000000..ead1b8ee1eb --- /dev/null +++ b/crates/circuit/src/operations.rs @@ -0,0 +1,786 @@ +// 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 std::f64::consts::PI; + +use crate::circuit_data::CircuitData; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::{gate_matrix, Qubit}; + +use ndarray::{aview2, Array2}; +use num_complex::Complex64; +use numpy::IntoPyArray; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::{intern, IntoPy, Python}; +use smallvec::smallvec; + +/// Valid types for an operation field in a CircuitInstruction +/// +/// These are basically the types allowed in a QuantumCircuit +#[derive(FromPyObject, Clone, Debug)] +pub enum OperationType { + Standard(StandardGate), + Instruction(PyInstruction), + Gate(PyGate), + Operation(PyOperation), +} + +impl Operation for OperationType { + fn name(&self) -> &str { + match self { + Self::Standard(op) => op.name(), + Self::Gate(op) => op.name(), + Self::Instruction(op) => op.name(), + Self::Operation(op) => op.name(), + } + } + + fn num_qubits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_qubits(), + Self::Gate(op) => op.num_qubits(), + Self::Instruction(op) => op.num_qubits(), + Self::Operation(op) => op.num_qubits(), + } + } + fn num_clbits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_clbits(), + Self::Gate(op) => op.num_clbits(), + Self::Instruction(op) => op.num_clbits(), + Self::Operation(op) => op.num_clbits(), + } + } + + fn num_params(&self) -> u32 { + match self { + Self::Standard(op) => op.num_params(), + Self::Gate(op) => op.num_params(), + Self::Instruction(op) => op.num_params(), + Self::Operation(op) => op.num_params(), + } + } + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::Standard(op) => op.matrix(params), + Self::Gate(op) => op.matrix(params), + Self::Instruction(op) => op.matrix(params), + Self::Operation(op) => op.matrix(params), + } + } + + fn control_flow(&self) -> bool { + match self { + Self::Standard(op) => op.control_flow(), + Self::Gate(op) => op.control_flow(), + Self::Instruction(op) => op.control_flow(), + Self::Operation(op) => op.control_flow(), + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::Standard(op) => op.definition(params), + Self::Gate(op) => op.definition(params), + Self::Instruction(op) => op.definition(params), + Self::Operation(op) => op.definition(params), + } + } + + fn standard_gate(&self) -> Option { + match self { + Self::Standard(op) => op.standard_gate(), + Self::Gate(op) => op.standard_gate(), + Self::Instruction(op) => op.standard_gate(), + Self::Operation(op) => op.standard_gate(), + } + } + + fn directive(&self) -> bool { + match self { + Self::Standard(op) => op.directive(), + Self::Gate(op) => op.directive(), + Self::Instruction(op) => op.directive(), + Self::Operation(op) => op.directive(), + } + } +} + +/// Trait for generic circuit operations these define the common attributes +/// needed for something to be addable to the circuit struct +pub trait Operation { + fn name(&self) -> &str; + fn num_qubits(&self) -> u32; + fn num_clbits(&self) -> u32; + fn num_params(&self) -> u32; + fn control_flow(&self) -> bool; + fn matrix(&self, params: &[Param]) -> Option>; + fn definition(&self, params: &[Param]) -> Option; + fn standard_gate(&self) -> Option; + fn directive(&self) -> bool; +} + +#[derive(Clone, Debug)] +pub enum Param { + ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +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()) + }, + ) + } +} + +impl IntoPy for Param { + fn into_py(self, py: Python) -> PyObject { + match &self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +impl ToPyObject for Param { + fn to_object(&self, py: Python) -> PyObject { + match self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[pyclass(module = "qiskit._accelerate.circuit")] +pub enum StandardGate { + ZGate = 0, + YGate = 1, + XGate = 2, + CZGate = 3, + CYGate = 4, + CXGate = 5, + CCXGate = 6, + RXGate = 7, + RYGate = 8, + RZGate = 9, + ECRGate = 10, + SwapGate = 11, + SXGate = 12, + GlobalPhaseGate = 13, + IGate = 14, + HGate = 15, + PhaseGate = 16, + UGate = 17, +} + +static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = + [1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1]; + +static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3]; + +static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ + "z", + "y", + "x", + "cz", + "cy", + "cx", + "ccx", + "rx", + "ry", + "rz", + "ecr", + "swap", + "sx", + "global_phase", + "id", + "h", + "p", + "u", +]; + +#[pymethods] +impl StandardGate { + pub fn copy(&self) -> Self { + *self + } + + // These pymethods are for testing: + pub fn _to_matrix(&self, py: Python, params: Vec) -> Option { + self.matrix(¶ms) + .map(|x| x.into_pyarray_bound(py).into()) + } + + pub fn _num_params(&self) -> u32 { + self.num_params() + } + + pub fn _get_definition(&self, params: Vec) -> Option { + self.definition(¶ms) + } + + #[getter] + pub fn get_num_qubits(&self) -> u32 { + self.num_qubits() + } + + #[getter] + pub fn get_num_clbits(&self) -> u32 { + self.num_clbits() + } + + #[getter] + pub fn get_num_params(&self) -> u32 { + self.num_params() + } + + #[getter] + pub fn get_name(&self) -> &str { + self.name() + } +} + +// This must be kept up-to-date with `StandardGate` when adding or removing +// gates from the enum +// +// Remove this when std::mem::variant_count() is stabilized (see +// https://github.com/rust-lang/rust/issues/73662 ) +pub const STANDARD_GATE_SIZE: usize = 18; + +impl Operation for StandardGate { + fn name(&self) -> &str { + STANDARD_GATE_NAME[*self as usize] + } + + fn num_qubits(&self) -> u32 { + STANDARD_GATE_NUM_QUBITS[*self as usize] + } + + fn num_params(&self) -> u32 { + STANDARD_GATE_NUM_PARAMS[*self as usize] + } + + fn num_clbits(&self) -> u32 { + 0 + } + + fn control_flow(&self) -> bool { + false + } + + fn directive(&self) -> bool { + false + } + + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::ZGate => match params { + [] => Some(aview2(&gate_matrix::Z_GATE).to_owned()), + _ => None, + }, + Self::YGate => match params { + [] => Some(aview2(&gate_matrix::Y_GATE).to_owned()), + _ => None, + }, + Self::XGate => match params { + [] => Some(aview2(&gate_matrix::X_GATE).to_owned()), + _ => None, + }, + Self::CZGate => match params { + [] => Some(aview2(&gate_matrix::CZ_GATE).to_owned()), + _ => None, + }, + Self::CYGate => match params { + [] => Some(aview2(&gate_matrix::CY_GATE).to_owned()), + _ => None, + }, + Self::CXGate => match params { + [] => Some(aview2(&gate_matrix::CX_GATE).to_owned()), + _ => None, + }, + Self::CCXGate => match params { + [] => Some(aview2(&gate_matrix::CCX_GATE).to_owned()), + _ => None, + }, + Self::RXGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()), + _ => None, + }, + Self::RYGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()), + _ => None, + }, + Self::RZGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), + _ => None, + }, + Self::ECRGate => match params { + [] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()), + _ => None, + }, + Self::SwapGate => match params { + [] => Some(aview2(&gate_matrix::SWAP_GATE).to_owned()), + _ => None, + }, + Self::SXGate => match params { + [] => Some(aview2(&gate_matrix::SX_GATE).to_owned()), + _ => None, + }, + Self::GlobalPhaseGate => match params { + [Param::Float(theta)] => { + Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) + } + _ => None, + }, + Self::IGate => match params { + [] => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()), + _ => None, + }, + Self::HGate => match params { + [] => Some(aview2(&gate_matrix::H_GATE).to_owned()), + _ => None, + }, + Self::PhaseGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()), + _ => None, + }, + Self::UGate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::ZGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::YGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![ + Param::Float(PI), + Param::Float(PI / 2.), + Param::Float(PI / 2.), + ], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CYGate => todo!("Add when we have S and S dagger"), + Self::CXGate => None, + Self::CCXGate => todo!("Add when we have T and TDagger"), + Self::RXGate => todo!("Add when we have R"), + Self::RYGate => todo!("Add when we have R"), + Self::RZGate => Python::with_gil(|py| -> Option { + match ¶ms[0] { + Param::Float(theta) => Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(*theta)], + smallvec![Qubit(0)], + )], + Param::Float(-0.5 * theta), + ) + .expect("Unexpected Qiskit python bug"), + ), + Param::ParameterExpression(theta) => Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::ParameterExpression(theta.clone_ref(py))], + smallvec![Qubit(0)], + )], + Param::ParameterExpression( + theta + .call_method1(py, intern!(py, "__rmul__"), (-0.5,)) + .expect("Parameter expression for global phase failed"), + ), + ) + .expect("Unexpected Qiskit python bug"), + ), + Param::Obj(_) => unreachable!(), + } + }), + Self::ECRGate => todo!("Add when we have RZX"), + Self::SwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SXGate => todo!("Add when we have S dagger"), + Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates(py, 0, [], params[0].clone()) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::IGate => None, + Self::HGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::PhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(0.), Param::Float(0.), params[0].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::UGate => None, + } + } + + fn standard_gate(&self) -> Option { + Some(*self) + } +} + +const FLOAT_ZERO: Param = Param::Float(0.0); + +/// This class is used to wrap a Python side Instruction that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyInstruction { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub instruction: PyObject, +} + +#[pymethods] +impl PyInstruction { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, instruction: PyObject) -> Self { + PyInstruction { + qubits, + clbits, + params, + op_name, + instruction, + } + } +} + +impl Operation for PyInstruction { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.instruction.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.instruction.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} + +/// This class is used to wrap a Python side Gate that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyGate { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub gate: PyObject, +} + +#[pymethods] +impl PyGate { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, gate: PyObject) -> Self { + PyGate { + qubits, + clbits, + params, + op_name, + gate, + } + } +} + +impl Operation for PyGate { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + Python::with_gil(|py| -> Option> { + match self.gate.getattr(py, intern!(py, "to_matrix")) { + Ok(to_matrix) => { + let res: Option = to_matrix.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let array: PyReadonlyArray2 = x.extract(py).ok()?; + Some(array.as_array().to_owned()) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn standard_gate(&self) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "_standard_gate")) { + Ok(stdgate) => match stdgate.extract(py) { + Ok(out_gate) => out_gate, + Err(_) => None, + }, + Err(_) => None, + } + }) + } + fn directive(&self) -> bool { + false + } +} + +/// This class is used to wrap a Python side Operation that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyOperation { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub operation: PyObject, +} + +#[pymethods] +impl PyOperation { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, operation: PyObject) -> Self { + PyOperation { + qubits, + clbits, + params, + op_name, + operation, + } + } +} + +impl Operation for PyOperation { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + None + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.operation.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs deleted file mode 100644 index 0c793f2b640..00000000000 --- a/crates/circuit/src/packed_instruction.rs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 crate::interner::Index; -use pyo3::prelude::*; - -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -pub(crate) struct PackedInstruction { - /// The Python-side operation instance. - pub op: PyObject, - /// The index under which the interner has stored `qubits`. - pub qubits_id: Index, - /// The index under which the interner has stored `clbits`. - pub clbits_id: Index, -} diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs new file mode 100644 index 00000000000..48c779eed3a --- /dev/null +++ b/crates/circuit/src/parameter_table.rs @@ -0,0 +1,173 @@ +// 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::prelude::*; +use pyo3::{import_exception, intern, PyObject}; + +import_exception!(qiskit.circuit.exceptions, CircuitError); + +use hashbrown::{HashMap, HashSet}; + +/// The index value in a `ParamEntry` that indicates the global phase. +pub const GLOBAL_PHASE_INDEX: usize = usize::MAX; + +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamEntryKeys { + keys: Vec<(usize, usize)>, + iter_pos: usize, +} + +#[pymethods] +impl ParamEntryKeys { + fn __iter__(slf: PyRef) -> Py { + slf.into() + } + + 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 + } else { + None + } + } +} + +#[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)>, +} + +impl ParamEntry { + pub fn add(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.insert((inst_index, param_index)); + } + + pub fn discard(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.remove(&(inst_index, param_index)); + } +} + +#[pymethods] +impl ParamEntry { + #[new] + pub fn new(inst_index: usize, param_index: usize) -> Self { + ParamEntry { + index_ids: HashSet::from([(inst_index, param_index)]), + } + } + + pub fn __len__(&self) -> usize { + self.index_ids.len() + } + + pub fn __contains__(&self, key: (usize, usize)) -> bool { + self.index_ids.contains(&key) + } + + pub fn __iter__(&self) -> ParamEntryKeys { + ParamEntryKeys { + keys: self.index_ids.iter().copied().collect(), + iter_pos: 0, + } + } +} + +#[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, +} + +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 + ))); + } + self.table.insert(uuid, entry); + self.names.insert(name, uuid); + self.uuid_map.insert(uuid, parameter); + Ok(()) + } + + pub fn discard_references( + &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); + } + } + } +} + +#[pymethods] +impl ParamTable { + #[new] + pub fn new() -> Self { + ParamTable { + table: HashMap::new(), + names: HashMap::new(), + uuid_map: HashMap::new(), + } + } + + pub fn clear(&mut self) { + self.table.clear(); + self.names.clear(); + self.uuid_map.clear(); + } + + pub fn pop(&mut self, key: u128, name: String) -> Option { + self.names.remove(&name); + self.uuid_map.remove(&key); + self.table.remove(&key) + } + + 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); + } + + 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)))? + } +} diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index daaf19e1f6a..413165e84b1 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] # crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the # feature a default, and run `cargo test --no-default-features` to turn it off. default = ["pyo3/extension-module"] +cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"] [dependencies] pyo3.workspace = true diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c6c95d27f92..bb0a30ea6af 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -57,7 +57,9 @@ def instructions(self) -> Sequence[CircuitInstruction]: """Indexable view onto the :class:`.CircuitInstruction`s backing this scope.""" @abc.abstractmethod - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate=False + ) -> CircuitInstruction: """Low-level 'append' primitive; this may assume that the qubits, clbits and operation are all valid for the circuit. @@ -420,7 +422,9 @@ def _raise_on_jump(operation): " because it is not in a loop." ) - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate: bool = False + ) -> CircuitInstruction: if self._forbidden_message is not None: raise CircuitError(self._forbidden_message) if not self._allow_jumps: diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e339cb8d94b..44155783d40 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -58,6 +58,7 @@ class Instruction(Operation): # Class attribute to treat like barrier for transpiler, unroller, drawer # NOTE: Using this attribute may change in the future (See issue # 5811) _directive = False + _standard_gate = None def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None): """Create a new instruction. diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index ac3d9fabd64..576d5dee826 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -140,13 +140,12 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc ) if self._requester is not None: classical = self._requester(classical) - for instruction in self._instructions: + for idx, instruction in enumerate(self._instructions): if isinstance(instruction, CircuitInstruction): updated = instruction.operation.c_if(classical, val) - if updated is not instruction.operation: - raise CircuitError( - "SingletonGate instances can only be added to InstructionSet via _add_ref" - ) + self._instructions[idx] = instruction.replace( + operation=updated, condition=updated.condition + ) else: data, idx = instruction instruction = data[idx] diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 2bbd5ca5650..16cc0e3dbaf 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -17,7 +17,7 @@ from qiskit._accelerate.circuit import CircuitData from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit.parametertable import ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView class BlueprintCircuit(QuantumCircuit, ABC): @@ -68,7 +68,6 @@ def _build(self) -> None: def _invalidate(self) -> None: """Invalidate the current circuit build.""" self._data = CircuitData(self._data.qubits, self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -88,7 +87,6 @@ def qregs(self, qregs): self._ancillas = [] self._qubit_indices = {} self._data = CircuitData(clbits=self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -122,10 +120,10 @@ def parameters(self) -> ParameterView: self._build() return super().parameters - def _append(self, instruction, _qargs=None, _cargs=None): + def _append(self, instruction, _qargs=None, _cargs=None, *, _standard_gate=False): if not self._is_built: self._build() - return super()._append(instruction, _qargs, _cargs) + return super()._append(instruction, _qargs, _cargs, _standard_gate=_standard_gate) def compose( self, diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 73bb1bb0389..f00c02df538 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -17,6 +17,7 @@ from qiskit.circuit._utils import with_gate_array from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key +from qiskit._accelerate.circuit import StandardGate from .rzx import RZXGate from .x import XGate @@ -84,6 +85,8 @@ class ECRGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.ECRGate + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new ECR gate.""" super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index ccd758e4724..59d6b56373d 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -20,6 +20,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class GlobalPhaseGate(Gate): @@ -36,6 +37,8 @@ class GlobalPhaseGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.GlobalPhaseGate + def __init__( self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index cc06a071a3f..2d273eed74d 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _H_ARRAY = 1 / sqrt(2) * numpy.array([[1, 1], [1, -1]], dtype=numpy.complex128) @@ -51,6 +52,8 @@ class HGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.HGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new H gate.""" super().__init__("h", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 93523215d6f..13a98ce0df8 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -15,6 +15,7 @@ from typing import Optional from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0], [0, 1]]) @@ -45,6 +46,8 @@ class IGate(SingletonGate): └───┘ """ + _standard_gate = StandardGate.IGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Identity gate.""" super().__init__("id", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 6de0307dc79..1a792649fea 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class PhaseGate(Gate): @@ -75,6 +76,8 @@ class PhaseGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.PhaseGate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index eaa73cf87c9..5579f9d3707 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXGate(Gate): @@ -50,6 +51,8 @@ class RXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 633a518bca7..e27398cc296 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYGate(Gate): @@ -49,6 +50,8 @@ class RYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 3040f956834..e8ee0f97603 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -17,6 +17,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZGate(Gate): @@ -59,6 +60,8 @@ class RZGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.RZGate + def __init__( self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 0e49783308c..243a84701ef 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SWAP_ARRAY = numpy.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) @@ -58,6 +59,8 @@ class SwapGate(SingletonGate): |a, b\rangle \rightarrow |b, a\rangle """ + _standard_gate = StandardGate.SwapGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SWAP gate.""" super().__init__("swap", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 0c003748a66..93ca85da019 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SX_ARRAY = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]] @@ -62,6 +63,8 @@ class SXGate(SingletonGate): """ + _standard_gate = StandardGate.SXGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SX gate.""" super().__init__("sx", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3d631898850..3495bc180f0 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class UGate(Gate): @@ -68,6 +69,8 @@ class UGate(Gate): U(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.UGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 7195df90dc9..6e959b3e62c 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _X_ARRAY = [[0, 1], [1, 0]] @@ -70,6 +71,8 @@ class XGate(SingletonGate): |1\rangle \rightarrow |0\rangle """ + _standard_gate = StandardGate.XGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new X gate.""" super().__init__("x", 1, [], label=label, duration=duration, unit=unit) @@ -212,6 +215,8 @@ class CXGate(SingletonControlledGate): `|a, b\rangle \rightarrow |a, a \oplus b\rangle` """ + _standard_gate = StandardGate.CXGate + def __init__( self, label: Optional[str] = None, @@ -362,6 +367,8 @@ class CCXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CCXGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index e69e1e2b794..d62586aa2b9 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _Y_ARRAY = [[0, -1j], [1j, 0]] @@ -70,6 +71,8 @@ class YGate(SingletonGate): |1\rangle \rightarrow -i|0\rangle """ + _standard_gate = StandardGate.YGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Y gate.""" super().__init__("y", 1, [], label=label, duration=duration, unit=unit) @@ -197,6 +200,8 @@ class CYGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CYGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 2b69595936d..19e4382cd84 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -20,6 +20,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate from .p import PhaseGate @@ -73,6 +74,8 @@ class ZGate(SingletonGate): |1\rangle \rightarrow -|1\rangle """ + _standard_gate = StandardGate.ZGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Z gate.""" super().__init__("z", 1, [], label=label, duration=duration, unit=unit) @@ -181,6 +184,8 @@ class CZGate(SingletonControlledGate): the target qubit if the control qubit is in the :math:`|1\rangle` state. """ + _standard_gate = StandardGate.CZGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index 6803126ec10..e5a41b1971c 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -12,197 +12,8 @@ """ Look-up table for variable parameters in QuantumCircuit. """ -import operator -import typing -from collections.abc import MappingView, MutableMapping, MutableSet - -class ParameterReferences(MutableSet): - """A set of instruction parameter slot references. - Items are expected in the form ``(instruction, param_index)``. Membership - testing is overridden such that items that are otherwise value-wise equal - are still considered distinct if their ``instruction``\\ s are referentially - distinct. - - In the case of the special value :attr:`.ParameterTable.GLOBAL_PHASE` for ``instruction``, the - ``param_index`` should be ``None``. - """ - - def _instance_key(self, ref): - return (id(ref[0]), ref[1]) - - def __init__(self, refs): - self._instance_ids = {} - - for ref in refs: - if not isinstance(ref, tuple) or len(ref) != 2: - raise ValueError("refs must be in form (instruction, param_index)") - k = self._instance_key(ref) - self._instance_ids[k] = ref[0] - - def __getstate__(self): - # Leave behind the reference IDs (keys of _instance_ids) since they'll - # be incorrect after unpickling on the other side. - return list(self) - - def __setstate__(self, refs): - # Recompute reference IDs for the newly unpickled instructions. - self._instance_ids = {self._instance_key(ref): ref[0] for ref in refs} - - def __len__(self): - return len(self._instance_ids) - - def __iter__(self): - for (_, idx), instruction in self._instance_ids.items(): - yield (instruction, idx) - - def __contains__(self, x) -> bool: - return self._instance_key(x) in self._instance_ids - - def __repr__(self) -> str: - return f"ParameterReferences({repr(list(self))})" - - def add(self, value): - """Adds a reference to the listing if it's not already present.""" - k = self._instance_key(value) - self._instance_ids[k] = value[0] - - def discard(self, value): - k = self._instance_key(value) - self._instance_ids.pop(k, None) - - def copy(self): - """Create a shallow copy.""" - return ParameterReferences(self) - - -class ParameterTable(MutableMapping): - """Class for tracking references to circuit parameters by specific - instruction instances. - - Keys are parameters. Values are of type :class:`~ParameterReferences`, - which overrides membership testing to be referential for instructions, - and is set-like. Elements of :class:`~ParameterReferences` - are tuples of ``(instruction, param_index)``. - """ - - __slots__ = ["_table", "_keys", "_names"] - - class _GlobalPhaseSentinel: - __slots__ = () - - def __copy__(self): - return self - - def __deepcopy__(self, memo=None): - return self - - def __reduce__(self): - return (operator.attrgetter("GLOBAL_PHASE"), (ParameterTable,)) - - def __repr__(self): - return "" - - GLOBAL_PHASE = _GlobalPhaseSentinel() - """Tracking object to indicate that a reference refers to the global phase of a circuit.""" - - def __init__(self, mapping=None): - """Create a new instance, initialized with ``mapping`` if provided. - - Args: - mapping (Mapping[Parameter, ParameterReferences]): - Mapping of parameter to the set of parameter slots that reference - it. - - Raises: - ValueError: A value in ``mapping`` is not a :class:`~ParameterReferences`. - """ - if mapping is not None: - if any(not isinstance(refs, ParameterReferences) for refs in mapping.values()): - raise ValueError("Values must be of type ParameterReferences") - self._table = mapping.copy() - else: - self._table = {} - - self._keys = set(self._table) - self._names = {x.name: x for x in self._table} - - def __getitem__(self, key): - return self._table[key] - - def __setitem__(self, parameter, refs): - """Associate a parameter with the set of parameter slots ``(instruction, param_index)`` - that reference it. - - .. note:: - - Items in ``refs`` are considered unique if their ``instruction`` is referentially - unique. See :class:`~ParameterReferences` for details. - - Args: - parameter (Parameter): the parameter - refs (Union[ParameterReferences, Iterable[(Instruction, int)]]): the parameter slots. - If this is an iterable, a new :class:`~ParameterReferences` is created from its - contents. - """ - if not isinstance(refs, ParameterReferences): - refs = ParameterReferences(refs) - - self._table[parameter] = refs - self._keys.add(parameter) - self._names[parameter.name] = parameter - - def get_keys(self): - """Return a set of all keys in the parameter table - - Returns: - set: A set of all the keys in the parameter table - """ - return self._keys - - def get_names(self): - """Return a set of all parameter names in the parameter table - - Returns: - set: A set of all the names in the parameter table - """ - return self._names.keys() - - def parameter_from_name(self, name: str, default: typing.Any = None): - """Get a :class:`.Parameter` with references in this table by its string name. - - If the parameter is not present, return the ``default`` value. - - Args: - name: The name of the :class:`.Parameter` - default: The object that should be returned if the parameter is missing. - """ - return self._names.get(name, default) - - def discard_references(self, expression, key): - """Remove all references to parameters contained within ``expression`` at the given table - ``key``. This also discards parameter entries from the table if they have no further - references. No action is taken if the object is not tracked.""" - for parameter in expression.parameters: - if (refs := self._table.get(parameter)) is not None: - if len(refs) == 1: - del self[parameter] - else: - refs.discard(key) - - def __delitem__(self, key): - del self._table[key] - self._keys.discard(key) - del self._names[key.name] - - def __iter__(self): - return iter(self._table) - - def __len__(self): - return len(self._table) - - def __repr__(self): - return f"ParameterTable({repr(self._table)})" +from collections.abc import MappingView class ParameterView(MappingView): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 606d0e04373..238a2682522 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ ) import numpy as np from qiskit._accelerate.circuit import CircuitData +from qiskit._accelerate.circuit import StandardGate, PyGate, PyInstruction, PyOperation from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -57,7 +58,7 @@ from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit -from .parametertable import ParameterReferences, ParameterTable, ParameterView +from .parametertable import ParameterView from .parametervector import ParameterVector from .instructionset import InstructionSet from .operation import Operation @@ -1124,14 +1125,10 @@ def __init__( self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict) self.add_register(*regs) - # Parameter table tracks instructions with variable parameters. - self._parameter_table = ParameterTable() - # Cache to avoid re-sorting parameters self._parameters = None self._layout = None - self._global_phase: ParameterValueType = 0 self.global_phase = global_phase # Add classical variables. Resolve inputs and captures first because they can't depend on @@ -1159,6 +1156,15 @@ def __init__( Qiskit will not examine the content of this mapping, but it will pass it through the transpiler and reattach it to the output, so you can track your own metadata.""" + @classmethod + def _from_circuit_data(cls, data: CircuitData) -> typing.Self: + """A private constructor from rust space circuit data.""" + out = QuantumCircuit() + out.add_bits(data.qubits) + out.add_bits(data.clbits) + out._data = data + return out + @staticmethod def from_instructions( instructions: Iterable[ @@ -1259,7 +1265,6 @@ def data(self, data_input: Iterable): data_input = list(data_input) self._data.clear() self._parameters = None - self._parameter_table = ParameterTable() # Repopulate the parameter table with any global-phase entries. self.global_phase = self.global_phase if not data_input: @@ -1382,12 +1387,11 @@ def __deepcopy__(self, memo=None): # Avoids pulling self._data into a Python list # like we would when pickling. - result._data = self._data.copy() + result._data = self._data.copy(deepcopy=True) result._data.replace_bits( qubits=_copy.deepcopy(self._data.qubits, memo), clbits=_copy.deepcopy(self._data.clbits, memo), ) - result._data.map_ops(lambda op: _copy.deepcopy(op, memo)) return result @classmethod @@ -1896,7 +1900,7 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: clbits = self.clbits[: other.num_clbits] if front: # Need to keep a reference to the data for use after we've emptied it. - old_data = dest._data.copy() + old_data = dest._data.copy(copy_instructions=copy) dest.clear() dest.append(other, qubits, clbits, copy=copy) for instruction in old_data: @@ -2024,14 +2028,14 @@ def map_vars(op): ) return n_op.copy() if n_op is op and copy else n_op - instructions = source._data.copy() + instructions = source._data.copy(copy_instructions=copy) instructions.replace_bits(qubits=new_qubits, clbits=new_clbits) instructions.map_ops(map_vars) dest._current_scope().extend(instructions) append_existing = None if front: - append_existing = dest._data.copy() + append_existing = dest._data.copy(copy_instructions=copy) dest.clear() copy_with_remapping( other, @@ -2296,6 +2300,35 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list clbit_representation, self.clbits, self._clbit_indices, Clbit ) + def _append_standard_gate( + self, + op: StandardGate, + params: Sequence[ParameterValueType] | None = None, + qargs: Sequence[QubitSpecifier] | None = None, + cargs: Sequence[ClbitSpecifier] | None = None, + label: str | None = None, + ) -> InstructionSet: + """An internal method to bypass some checking when directly appending a standard gate.""" + circuit_scope = self._current_scope() + + if params is None: + params = [] + + expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] + expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] + if params is not None: + for param in params: + Gate.validate_parameter(op, param) + + instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) + broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs) + for qarg, carg in broadcast_iter: + self._check_dups(qarg) + instruction = CircuitInstruction(op, qarg, carg, params=params, label=label) + circuit_scope.append(instruction, _standard_gate=True) + instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) + return instructions + def append( self, instruction: Operation | CircuitInstruction, @@ -2393,16 +2426,47 @@ def append( if isinstance(operation, Instruction) else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) ) + params = None + if isinstance(operation, Gate): + params = operation.params + operation = PyGate( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Instruction): + params = operation.params + operation = PyInstruction( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Operation): + params = getattr(operation, "params", ()) + operation = PyOperation( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + for qarg, carg in broadcast_iter: self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) + instruction = CircuitInstruction(operation, qarg, carg, params=params) circuit_scope.append(instruction) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions # Preferred new style. @typing.overload - def _append(self, instruction: CircuitInstruction) -> CircuitInstruction: ... + def _append( + self, instruction: CircuitInstruction, *, _standard_gate: bool + ) -> CircuitInstruction: ... # To-be-deprecated old style. @typing.overload @@ -2413,7 +2477,7 @@ def _append( cargs: Sequence[Clbit], ) -> Operation: ... - def _append(self, instruction, qargs=(), cargs=()): + def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = False): """Append an instruction to the end of the circuit, modifying the circuit in place. .. warning:: @@ -2454,40 +2518,39 @@ def _append(self, instruction, qargs=(), cargs=()): :meta public: """ + if _standard_gate: + new_param = self._data.append(instruction) + if new_param: + self._parameters = None + self.duration = None + self.unit = "dt" + return instruction + old_style = not isinstance(instruction, CircuitInstruction) if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) - self._data.append(instruction) - self._track_operation(instruction.operation) - return instruction.operation if old_style else instruction + # If there is a reference to the outer circuit in an + # 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. + try: + new_param = 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 - def _track_operation(self, operation: Operation): - """Sync all non-data-list internal data structures for a newly tracked operation.""" - if isinstance(operation, Instruction): - self._update_parameter_table(operation) + # Invalidate whole circuit duration if an instruction is added self.duration = None self.unit = "dt" - - def _update_parameter_table(self, instruction: Instruction): - for param_index, param in enumerate(instruction.params): - if isinstance(param, (ParameterExpression, QuantumCircuit)): - # Scoped constructs like the control-flow ops use QuantumCircuit as a parameter. - atomic_parameters = set(param.parameters) - else: - atomic_parameters = set() - - for parameter in atomic_parameters: - if parameter in self._parameter_table: - self._parameter_table[parameter].add((instruction, param_index)) - else: - if parameter.name in self._parameter_table.get_names(): - raise CircuitError(f"Name conflict on adding parameter: {parameter.name}") - self._parameter_table[parameter] = ParameterReferences( - ((instruction, param_index),) - ) - - # clear cache if new parameter is added - self._parameters = None + return instruction.operation if old_style else instruction @typing.overload def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ... @@ -2538,7 +2601,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._parameter_table.parameter_from_name(name, None)) is None: + if (parameter := self._data.get_param_from_name(name)) is None: if default is Ellipsis: raise KeyError(f"no parameter named '{name}' is present") return default @@ -3415,13 +3478,7 @@ def num_nonlocal_gates(self) -> int: Conditional nonlocal gates are also included. """ - multi_qubit_gates = 0 - for instruction in self._data: - if instruction.operation.num_qubits > 1 and not getattr( - instruction.operation, "_directive", False - ): - multi_qubit_gates += 1 - return multi_qubit_gates + return self._data.num_nonlocal_gates() def get_instructions(self, name: str) -> list[CircuitInstruction]: """Get instructions matching name. @@ -3535,29 +3592,6 @@ def copy(self, name: str | None = None) -> typing.Self: """ cpy = self.copy_empty_like(name) cpy._data = self._data.copy() - - # The special global-phase sentinel doesn't need copying, but it's - # added here to ensure it's recognised. The global phase itself was - # already copied over in `copy_empty_like`. - operation_copies = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} - - def memo_copy(op): - if (out := operation_copies.get(id(op))) is not None: - return out - copied = op.copy() - operation_copies[id(op)] = copied - return copied - - cpy._data.map_ops(memo_copy) - cpy._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_copies[id(operation)], param_index) - for operation, param_index in self._parameter_table[param] - ) - for param in self._parameter_table - } - ) return cpy def copy_empty_like( @@ -3636,12 +3670,9 @@ def copy_empty_like( else: # pragma: no cover raise ValueError(f"unknown vars_mode: '{vars_mode}'") - cpy._parameter_table = ParameterTable() - for parameter in getattr(cpy.global_phase, "parameters", ()): - cpy._parameter_table[parameter] = ParameterReferences( - [(ParameterTable.GLOBAL_PHASE, None)] - ) - cpy._data = CircuitData(self._data.qubits, self._data.clbits) + cpy._data = CircuitData( + self._data.qubits, self._data.clbits, global_phase=self._data.global_phase + ) cpy._calibrations = _copy.deepcopy(self._calibrations) cpy._metadata = _copy.deepcopy(self._metadata) @@ -3661,7 +3692,6 @@ def clear(self) -> None: quantum and classical typed data, but without mutating the original circuit. """ self._data.clear() - self._parameter_table.clear() # Repopulate the parameter table with any phase symbols. self.global_phase = self.global_phase @@ -3945,10 +3975,9 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi circ._clbit_indices = {} # Clear instruction info - circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data)) - circ._parameter_table.clear() - # Repopulate the parameter table with any global-phase entries. - circ.global_phase = circ.global_phase + circ._data = CircuitData( + qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase + ) # We must add the clbits first to preserve the original circuit # order. This way, add_register never adds clbits and just @@ -4021,7 +4050,7 @@ def global_phase(self) -> ParameterValueType: """The global phase of the current circuit scope in radians.""" if self._control_flow_scopes: return self._control_flow_scopes[-1].global_phase - return self._global_phase + return self._data.global_phase @global_phase.setter def global_phase(self, angle: ParameterValueType): @@ -4032,23 +4061,18 @@ def global_phase(self, angle: ParameterValueType): """ # 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 initialised. - global_phase_reference = (ParameterTable.GLOBAL_PHASE, None) - if isinstance(previous := getattr(self, "_global_phase", None), ParameterExpression): + if isinstance(getattr(self._data, "global_phase", None), ParameterExpression): self._parameters = None - self._parameter_table.discard_references(previous, global_phase_reference) - - if isinstance(angle, ParameterExpression) and angle.parameters: - for parameter in angle.parameters: - if parameter not in self._parameter_table: - self._parameters = None - self._parameter_table[parameter] = ParameterReferences(()) - self._parameter_table[parameter].add(global_phase_reference) + 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: - self._global_phase = angle + self._data.global_phase = angle @property def parameters(self) -> ParameterView: @@ -4118,7 +4142,7 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: """The number of parameter objects in the circuit.""" - return len(self._parameter_table) + return self._data.num_params() def _unsorted_parameters(self) -> set[Parameter]: """Efficiently get all parameters in the circuit, without any sorting overhead. @@ -4131,7 +4155,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._parameter_table.get_keys() + return self._data.get_params_unsorted() @overload def assign_parameters( @@ -4280,7 +4304,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc target._parameters = None # This is deliberately eager, because we want the side effect of clearing the table. all_references = [ - (parameter, value, target._parameter_table.pop(parameter, ())) + (parameter, value, target._data.pop_param(parameter.uuid.int, parameter.name, ())) for parameter, value in parameter_binds.items() ] seen_operations = {} @@ -4291,20 +4315,28 @@ def assign_parameters( # pylint: disable=missing-raises-doc if isinstance(bound_value, ParameterExpression) else () ) - for operation, index in references: - seen_operations[id(operation)] = operation - if operation is ParameterTable.GLOBAL_PHASE: + 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 parameter not in target._parameter_table: - target._parameter_table[parameter] = ParameterReferences(()) - target._parameter_table[parameter].add((operation, index)) + 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): @@ -4316,12 +4348,18 @@ def assign_parameters( # pylint: disable=missing-raises-doc f"Saw an unknown type during symbolic binding: {assignee}." " This may indicate an internal logic error in symbol tracking." ) - if operation is ParameterTable.GLOBAL_PHASE: + 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._global_phase = new_parameter + target._data.global_phase = new_parameter else: - operation.params[index] = new_parameter + 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. @@ -4368,6 +4406,7 @@ 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( @@ -4450,9 +4489,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.h import HGate - - return self.append(HGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.HGate, [], qargs=[qubit]) def ch( self, @@ -4496,9 +4533,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali Returns: A handle to the instructions created. """ - from .library.standard_gates.i import IGate - - return self.append(IGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.IGate, None, qargs=[qubit]) def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.MSGate`. @@ -4529,9 +4564,7 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.p import PhaseGate - - return self.append(PhaseGate(theta), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.PhaseGate, [theta], qargs=[qubit]) def cp( self, @@ -4712,9 +4745,7 @@ def rx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rx import RXGate - - return self.append(RXGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], None, label=label) def crx( self, @@ -4783,9 +4814,7 @@ def ry( Returns: A handle to the instructions created. """ - from .library.standard_gates.ry import RYGate - - return self.append(RYGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], None, label=label) def cry( self, @@ -4851,9 +4880,7 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.rz import RZGate - - return self.append(RZGate(phi), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RZGate, [phi], [qubit], None) def crz( self, @@ -4937,9 +4964,9 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.ecr import ECRGate - - return self.append(ECRGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate( + StandardGate.ECRGate, [], qargs=[qubit1, qubit2], cargs=None + ) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -5044,9 +5071,12 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet Returns: A handle to the instructions created. """ - from .library.standard_gates.swap import SwapGate - - return self.append(SwapGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate( + StandardGate.SwapGate, + [], + qargs=[qubit1, qubit2], + cargs=None, + ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.iSwapGate`. @@ -5107,9 +5137,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.sx import SXGate - - return self.append(SXGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SXGate, None, qargs=[qubit]) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -5207,9 +5235,7 @@ def u( Returns: A handle to the instructions created. """ - from .library.standard_gates.u import UGate - - return self.append(UGate(theta, phi, lam), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.UGate, [theta, phi, lam], qargs=[qubit]) def cu( self, @@ -5262,9 +5288,7 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.x import XGate - - return self.append(XGate(label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.XGate, None, qargs=[qubit], label=label) def cx( self, @@ -5288,14 +5312,17 @@ def cx( Returns: A handle to the instructions created. """ + if ctrl_state is not None: + from .library.standard_gates.x import CXGate - from .library.standard_gates.x import CXGate - - return self.append( - CXGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CXGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + return self._append_standard_gate( + StandardGate.CXGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5336,13 +5363,20 @@ def ccx( Returns: A handle to the instructions created. """ - from .library.standard_gates.x import CCXGate + if ctrl_state is not None: + from .library.standard_gates.x import CCXGate - return self.append( - CCXGate(ctrl_state=ctrl_state), - [control_qubit1, control_qubit2, target_qubit], + return self.append( + CCXGate(ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], + [], + copy=False, + ) + return self._append_standard_gate( + StandardGate.CCXGate, [], - copy=False, + qargs=[control_qubit1, control_qubit2, target_qubit], + cargs=None, ) def mcx( @@ -5440,9 +5474,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.y import YGate - - return self.append(YGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.YGate, None, qargs=[qubit]) def cy( self, @@ -5466,13 +5498,18 @@ def cy( Returns: A handle to the instructions created. """ - from .library.standard_gates.y import CYGate + if ctrl_state is not None: + from .library.standard_gates.y import CYGate - return self.append( - CYGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CYGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + + return self._append_standard_gate( + StandardGate.CYGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def z(self, qubit: QubitSpecifier) -> InstructionSet: @@ -5486,9 +5523,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.z import ZGate - - return self.append(ZGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.ZGate, None, qargs=[qubit]) def cz( self, @@ -5512,13 +5547,18 @@ def cz( Returns: A handle to the instructions created. """ - from .library.standard_gates.z import CZGate + if ctrl_state is not None: + from .library.standard_gates.z import CZGate - return self.append( - CZGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CZGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + + return self._append_standard_gate( + StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def ccz( @@ -5907,36 +5947,9 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: if not self._data: raise CircuitError("This circuit contains no instructions.") instruction = self._data.pop() - if isinstance(instruction.operation, Instruction): - self._update_parameter_table_on_instruction_removal(instruction) + self._parameters = None return instruction - def _update_parameter_table_on_instruction_removal(self, instruction: CircuitInstruction): - """Update the :obj:`.ParameterTable` of this circuit given that an instance of the given - ``instruction`` has just been removed from the circuit. - - .. note:: - - This does not account for the possibility for the same instruction instance being added - more than once to the circuit. At the time of writing (2021-11-17, main commit 271a82f) - there is a defensive ``deepcopy`` of parameterised instructions inside - :meth:`.QuantumCircuit.append`, so this should be safe. Trying to account for it would - involve adding a potentially quadratic-scaling loop to check each entry in ``data``. - """ - atomic_parameters: list[tuple[Parameter, int]] = [] - for index, parameter in enumerate(instruction.operation.params): - if isinstance(parameter, (ParameterExpression, QuantumCircuit)): - atomic_parameters.extend((p, index) for p in parameter.parameters) - for atomic_parameter, index in atomic_parameters: - new_entries = self._parameter_table[atomic_parameter].copy() - new_entries.discard((instruction.operation, index)) - if not new_entries: - del self._parameter_table[atomic_parameter] - # Invalidate cache. - self._parameters = None - else: - self._parameter_table[atomic_parameter] = new_entries - @typing.overload def while_loop( self, @@ -6582,13 +6595,15 @@ def __init__(self, circuit: QuantumCircuit): def instructions(self): return self.circuit._data - def append(self, instruction): + def append(self, instruction, *, _standard_gate: bool = False): # QuantumCircuit._append is semi-public, so we just call back to it. - return self.circuit._append(instruction) + return self.circuit._append(instruction, _standard_gate=_standard_gate) def extend(self, data: CircuitData): self.circuit._data.extend(data) - data.foreach_op(self.circuit._track_operation) + self.circuit._parameters = None + self.circuit.duration = None + self.circuit.unit = "dt" def resolve_classical_resource(self, specifier): # This is slightly different to cbit_argument_conversion, because it should not diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 3e29f36c6be..9ecc8e6a6ca 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -45,8 +45,6 @@ def __setitem__(self, key, value): operation, qargs, cargs = value value = self._resolve_legacy_value(operation, qargs, cargs) self._circuit._data[key] = value - if isinstance(value.operation, Instruction): - self._circuit._update_parameter_table(value.operation) def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: """Resolve the old-style 3-tuple into the new :class:`CircuitInstruction` type.""" @@ -76,7 +74,7 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: return CircuitInstruction(operation, tuple(qargs), tuple(cargs)) def insert(self, index, value): - self._circuit._data.insert(index, CircuitInstruction(None, (), ())) + self._circuit._data.insert(index, value.replace(qubits=(), clbits=())) try: self[index] = value except CircuitError: diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 2bdcbfef358..1a5907c3ec8 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Helper function for converting a circuit to an instruction.""" -from qiskit.circuit.parametertable import ParameterTable, ParameterReferences from qiskit.exceptions import QiskitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumregister import QuantumRegister @@ -121,7 +120,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None regs.append(creg) clbit_map = {bit: creg[idx] for idx, bit in enumerate(circuit.clbits)} - operation_map = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} + operation_map = {} def fix_condition(op): original_id = id(op) @@ -149,15 +148,6 @@ def fix_condition(op): qc = QuantumCircuit(*regs, name=out_instruction.name) qc._data = data - qc._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_map[id(operation)], param_index) - for operation, param_index in target._parameter_table[param] - ) - for param in target._parameter_table - } - ) if circuit.global_phase: qc.global_phase = circuit.global_phase diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 16bc2529ca7..7f737af76eb 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -252,6 +252,9 @@ def __init__(self, includelist, basis_gates=()): def __setitem__(self, name_str, instruction): self._data[name_str] = instruction.base_class self._data[id(instruction)] = name_str + ctrl_state = str(getattr(instruction, "ctrl_state", "")) + + self._data[f"{instruction.name}_{ctrl_state}_{instruction.params}"] = name_str def __getitem__(self, key): if isinstance(key, Instruction): @@ -262,7 +265,9 @@ def __getitem__(self, key): pass # Built-in gates. if key.name not in self._data: - raise KeyError(key) + # Registerd qiskit standard gate without stgates.inc + ctrl_state = str(getattr(key, "ctrl_state", "")) + return self._data[f"{key.name}_{ctrl_state}_{key.params}"] return key.name return self._data[key] @@ -1102,7 +1107,8 @@ 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 instruction, index in circuit._parameter_table[parameter]: + for instr_index, index in circuit._data._get_param(parameter.uuid.int): + instruction = circuit.data[instr_index].operation if isinstance(instruction, ForLoopOp): # The parameters of ForLoopOp are (indexset, loop_parameter, body). if index == 1: diff --git a/qiskit/quantum_info/operators/dihedral/dihedral.py b/qiskit/quantum_info/operators/dihedral/dihedral.py index 4f49879063e..75b455410f4 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral.py @@ -452,8 +452,7 @@ def conjugate(self): new_qubits = [bit_indices[tup] for tup in instruction.qubits] if instruction.operation.name == "p": params = 2 * np.pi - instruction.operation.params[0] - instruction.operation.params[0] = params - new_circ.append(instruction.operation, new_qubits) + new_circ.p(params, new_qubits) elif instruction.operation.name == "t": instruction.operation.name = "tdg" new_circ.append(instruction.operation, new_qubits) diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 7cb309dd9aa..806e001f2bd 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -361,17 +361,17 @@ def _pad( theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): # Absorb the inverse into the successor (from left in circuit) - theta_r, phi_r, lam_r = next_node.op.params - next_node.op.params = Optimize1qGates.compose_u3( - theta_r, phi_r, lam_r, theta, phi, lam - ) + op = next_node.op + theta_r, phi_r, lam_r = op.params + op.params = Optimize1qGates.compose_u3(theta_r, phi_r, lam_r, theta, phi, lam) + next_node.op = op sequence_gphase += phase elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): # Absorb the inverse into the predecessor (from right in circuit) - theta_l, phi_l, lam_l = prev_node.op.params - prev_node.op.params = Optimize1qGates.compose_u3( - theta, phi, lam, theta_l, phi_l, lam_l - ) + op = prev_node.op + theta_l, phi_l, lam_l = op.params + op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l) + prev_node.op = op sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 3792a149fd7..69bea32acca 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,8 +70,9 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) - node.op = node.op.to_mutable() - node.op.duration = duration + op = node.op.to_mutable() + op.duration = duration + node.op = op else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 25672c137f3..08ac932d8ae 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -105,9 +105,10 @@ def run(self, dag: DAGCircuit): ) except TranspilerError: continue - node.op = node.op.to_mutable() - node.op.duration = duration - node.op.unit = time_unit + op = node.op.to_mutable() + op.duration = duration + op.unit = time_unit + node.op = op self.property_set["time_unit"] = time_unit return dag diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml new file mode 100644 index 00000000000..d826bc15e48 --- /dev/null +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -0,0 +1,79 @@ +--- +features_circuits: + - | + A native rust representation of Qiskit's standard gate library has been added. When a standard gate + is added to a :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` it is now represented in a more + efficient manner directly in Rust seamlessly. Accessing that gate object from a circuit or dag will + return a new Python object representing the standard gate. This leads to faster and more efficient + transpilation and manipulation of circuits for functionality written in Rust. +features_misc: + - | + Added a new build-time environment variable ``QISKIT_NO_CACHE_GATES`` which + when set to a value of ``1`` (i.e. ``QISKIT_NO_CACHE_GATES=1``) which + decreases the memory overhead of a :class:`.CircuitInstruction` and + :class:`.DAGOpNode` object at the cost of decreased runtime on multiple + accesses to :attr:`.CircuitInstruction.operation` and :attr:`.DAGOpNode.op`. + If this environment variable is set when building the Qiskit python package + from source the caching of the return of these attributes will be disabled. +upgrade_circuits: + - | + The :class:`.Operation` instances of :attr:`.DAGOpNode.op` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.DAGOpNode.op` directly by reference + was unsound and always likely to corrupt the dag's internal state tracking + Due to the internal refactor of the :class:`.QuantumCircuit` and + :class:`.DAGCircuit` to store standard gates in rust the output object from + :attr:`.DAGOpNode.op` will now likely be a copy instead of a shared instance. If you + need to mutate an element should ensure that you either do:: + + op = dag_node.op + op.params[0] = 3.14159 + dag_node.op = op + + or:: + + op = dag_node.op + op.params[0] = 3.14159 + dag.substitute_node(dag_node, op) + + instead of doing something like:: + + dag_node.op.params[0] = 3.14159 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. + - | + The :class:`.Operation` instances of :attr:`.CircuitInstruction.operation` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.CircuitInstruction.operation` directly by reference + was unsound and always likely to corrupt the circuit, especially when + parameters were in use. Due to the internal refactor of the QuantumCircuit + to store standard gates in rust the output object from + :attr:`.CircuitInstruction.operation` will now likely be a copy instead + of a shared instance. If you need to mutate an element in the circuit (which + is strongly **not** recommended as it's inefficient and error prone) you + should ensure that you do:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + op = qc.data[0].operation + op.params[0] = 3.14 + + qc.data[0] = qc.data[0].replace(operation=op) + + instead of doing something like:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + qc.data[0].operation.params[0] = 3.14 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. diff --git a/requirements-optional.txt b/requirements-optional.txt index 36985cdd7cd..3dfc2031d02 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -19,7 +19,7 @@ seaborn>=0.9.0 # Functionality and accelerators. qiskit-aer -qiskit-qasm3-import +qiskit-qasm3-import>=0.5.0 python-constraint>=1.4 cvxpy scikit-learn>=0.20.0 diff --git a/setup.py b/setup.py index 9bb5b04ae6e..38af5286e81 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,17 @@ # it's an editable installation. rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None +# If QISKIT_NO_CACHE_GATES is set then don't enable any features while building +# +# TODO: before final release we should reverse this by default once the default transpiler pass +# is all in rust (default to no caching and make caching an opt-in feature). This is opt-out +# right now to avoid the runtime overhead until we are leveraging the rust gates infrastructure. +if os.getenv("QISKIT_NO_CACHE_GATES") == "1": + features = [] +else: + features = ["cache_pygates"] + + setup( rust_extensions=[ RustExtension( @@ -37,6 +48,7 @@ "crates/pyext/Cargo.toml", binding=Binding.PyO3, debug=rust_debug, + features=features, ) ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, diff --git a/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 2a5070e8ac7..5f0a2814872 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(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) mock._invalidate() with self.subTest(msg="after invalidating"): self.assertFalse(mock._is_built) - self.assertEqual(len(mock._parameter_table), 0) + self.assertEqual(mock._data.num_params(), 0) mock._build() with self.subTest(msg="after re-building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) def test_calling_attributes_works(self): """Test that the circuit is constructed when attributes are called.""" diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 73398e4316b..6fc6e8e72bd 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -187,12 +187,20 @@ def test_foreach_op_indexed(self): def test_map_ops(self): """Test all operations are replaced.""" qr = QuantumRegister(5) + + # Use a custom gate to ensure we get a gate class returned and not + # a standard gate. + class CustomXGate(XGate): + """A custom X gate that doesn't have rust native representation.""" + + _standard_gate = None + data_list = [ - CircuitInstruction(XGate(), [qr[0]], []), - CircuitInstruction(XGate(), [qr[1]], []), - CircuitInstruction(XGate(), [qr[2]], []), - CircuitInstruction(XGate(), [qr[3]], []), - CircuitInstruction(XGate(), [qr[4]], []), + CircuitInstruction(CustomXGate(), [qr[0]], []), + CircuitInstruction(CustomXGate(), [qr[1]], []), + CircuitInstruction(CustomXGate(), [qr[2]], []), + CircuitInstruction(CustomXGate(), [qr[3]], []), + CircuitInstruction(CustomXGate(), [qr[4]], []), ] data = CircuitData(qubits=list(qr), data=data_list) data.map_ops(lambda op: op.to_mutable()) @@ -828,6 +836,9 @@ def test_param_gate_instance(self): qc0.append(rx, [0]) qc1.append(rx, [0]) qc0.assign_parameters({a: b}, inplace=True) - qc0_instance = next(iter(qc0._parameter_table[b]))[0] - qc1_instance = next(iter(qc1._parameter_table[a]))[0] + # 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]] 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 6caf194d37d..e9a7416f78c 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(len(qc._parameter_table), 0) + self.assertEqual(qc._data.num_params(), 0) def test_barrier(self): """Test multiple argument forms of barrier.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index db6280b8823..7bb36a1401f 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -357,7 +357,8 @@ def test_compose_copy(self): self.assertIsNot(should_copy.data[-1].operation, parametric.data[-1].operation) self.assertEqual(should_copy.data[-1].operation, parametric.data[-1].operation) forbid_copy = base.compose(parametric, qubits=[0], copy=False) - self.assertIs(forbid_copy.data[-1].operation, parametric.data[-1].operation) + # For standard gates a fresh copy is returned from the data list each time + self.assertEqual(forbid_copy.data[-1].operation, parametric.data[-1].operation) conditional = QuantumCircuit(1, 1) conditional.x(0).c_if(conditional.clbits[0], True) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index edd01c5cc1c..4ac69278fd4 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -577,14 +577,14 @@ def test_instructionset_c_if_with_no_requester(self): instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) instructions.c_if(register, 0) - self.assertIs(instruction.condition[0], register) + self.assertIs(instructions[0].operation.condition[0], register) with self.subTest("accepts arbitrary bit"): instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) bit = Clbit() instructions.c_if(bit, 0) - self.assertIs(instruction.condition[0], bit) + self.assertIs(instructions[0].operation.condition[0], bit) with self.subTest("rejects index"): instruction = RZGate(0) instructions = InstructionSet() @@ -617,7 +617,7 @@ def dummy_requester(specifier): bit = Clbit() instructions.c_if(bit, 0) dummy_requester.assert_called_once_with(bit) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -626,7 +626,7 @@ def dummy_requester(specifier): index = 0 instructions.c_if(index, 0) dummy_requester.assert_called_once_with(index) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -635,7 +635,7 @@ def dummy_requester(specifier): register = ClassicalRegister(2) instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) with self.subTest("calls requester only once when broadcast"): dummy_requester.reset_mock() instruction_list = [RZGate(0), RZGate(0), RZGate(0)] @@ -646,7 +646,7 @@ def dummy_requester(specifier): instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) for instruction in instruction_list: - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) def test_label_type_enforcement(self): """Test instruction label type enforcement.""" diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index a09ff331e02..35ff639cedd 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -102,7 +102,6 @@ def test_isometry_tolerance(self, iso): # Simulate the decomposed gate unitary = Operator(qc).data iso_from_circuit = unitary[::, 0 : 2**num_q_input] - self.assertTrue(np.allclose(iso_from_circuit, iso)) @data( diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c86deee4287..fd63057cff8 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -26,7 +26,7 @@ from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Instruction, Parameter, ParameterExpression, ParameterVector -from qiskit.circuit.parametertable import ParameterReferences, ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.exceptions import CircuitError from qiskit.compiler import assemble, transpile from qiskit import pulse @@ -45,8 +45,6 @@ def raise_if_parameter_table_invalid(circuit): CircuitError: if QuantumCircuit and ParameterTable are inconsistent. """ - table = circuit._parameter_table - # Assert parameters present in circuit match those in table. circuit_parameters = { parameter @@ -55,7 +53,7 @@ def raise_if_parameter_table_invalid(circuit): for parameter in param.parameters if isinstance(param, ParameterExpression) } - table_parameters = set(table._table.keys()) + table_parameters = set(circuit._data.get_params_unsorted()) if circuit_parameters != table_parameters: raise CircuitError( @@ -67,8 +65,10 @@ def raise_if_parameter_table_invalid(circuit): # Assert parameter locations in table are present in circuit. circuit_instructions = [instr.operation for instr in circuit._data] - for parameter, instr_list in table.items(): - for instr, param_index in instr_list: + for parameter in table_parameters: + instr_list = circuit._data._get_param(parameter.uuid.int) + 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}.") @@ -88,13 +88,15 @@ def raise_if_parameter_table_invalid(circuit): ) # Assert circuit has no other parameter locations other than those in table. - for instruction in circuit._data: + for instr_index, instruction in enumerate(circuit._data): for param_index, param in enumerate(instruction.operation.params): if isinstance(param, ParameterExpression): parameters = param.parameters for parameter in parameters: - if (instruction.operation, param_index) not in table[parameter]: + if (instr_index, param_index) not in circuit._data._get_param( + parameter.uuid.int + ): raise CircuitError( "Found parameterized instruction not " "present in table. Instruction: {} " @@ -158,15 +160,19 @@ def test_append_copies_parametric(self): self.assertIsNot(qc.data[-1].operation, gate_param) self.assertEqual(qc.data[-1].operation, gate_param) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_param, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_param) + self.assertEqual(qc.data[-1].operation, gate_param) qc.append(gate_expr, [0], copy=True) self.assertIsNot(qc.data[-1].operation, gate_expr) self.assertEqual(qc.data[-1].operation, gate_expr) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_expr, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_expr) + self.assertEqual(qc.data[-1].operation, gate_expr) def test_parameters_property(self): """Test instantiating gate with variable parameters""" @@ -177,10 +183,9 @@ def test_parameters_property(self): qc = QuantumCircuit(qr) rxg = RXGate(theta) qc.append(rxg, [qr[0]], []) - vparams = qc._parameter_table - self.assertEqual(len(vparams), 1) - self.assertIs(theta, next(iter(vparams))) - self.assertEqual(rxg, next(iter(vparams[theta]))[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) def test_parameters_property_by_index(self): """Test getting parameters by index""" @@ -553,12 +558,12 @@ def test_two_parameter_expression_binding(self): qc.rx(theta, 0) qc.ry(phi, 0) - self.assertEqual(len(qc._parameter_table[theta]), 1) - self.assertEqual(len(qc._parameter_table[phi]), 1) + self.assertEqual(qc._data._get_entry_count(theta), 1) + self.assertEqual(qc._data._get_entry_count(phi), 1) qc.assign_parameters({theta: -phi}, inplace=True) - self.assertEqual(len(qc._parameter_table[phi]), 2) + self.assertEqual(qc._data._get_entry_count(phi), 2) def test_expression_partial_binding_zero(self): """Verify that binding remains possible even if a previous partial bind @@ -580,7 +585,6 @@ def test_expression_partial_binding_zero(self): fbqc = pqc.assign_parameters({phi: 1}) self.assertEqual(fbqc.parameters, set()) - self.assertIsInstance(fbqc.data[0].operation.params[0], int) self.assertEqual(float(fbqc.data[0].operation.params[0]), 0) def test_raise_if_assigning_params_not_in_circuit(self): @@ -614,7 +618,7 @@ def test_gate_multiplicity_binding(self): qc.append(gate, [0], []) qc.append(gate, [0], []) qc2 = qc.assign_parameters({theta: 1.0}) - self.assertEqual(len(qc2._parameter_table), 0) + self.assertEqual(qc2._data.num_params(), 0) for instruction in qc2.data: self.assertEqual(float(instruction.operation.params[0]), 1.0) @@ -2170,155 +2174,6 @@ def test_parameter_symbol_equal_after_ufunc(self): self.assertEqual(phi._parameter_symbols, cos_phi._parameter_symbols) -class TestParameterReferences(QiskitTestCase): - """Test the ParameterReferences class.""" - - def test_equal_inst_diff_instance(self): - """Different value equal instructions are treated as distinct.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - # test __contains__ - self.assertIn((gate1, 0), refs) - self.assertIn((gate2, 0), refs) - - gate_ids = {id(gate1), id(gate2)} - self.assertEqual(gate_ids, {id(gate) for gate, _ in refs}) - self.assertTrue(all(idx == 0 for _, idx in refs)) - - def test_pickle_unpickle(self): - """Membership testing after pickle/unpickle.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - to_pickle = (gate1, refs) - pickled = pickle.dumps(to_pickle) - (gate1_new, refs_new) = pickle.loads(pickled) - - self.assertEqual(len(refs_new), len(refs)) - self.assertNotIn((gate1, 0), refs_new) - self.assertIn((gate1_new, 0), refs_new) - - def test_equal_inst_same_instance(self): - """Referentially equal instructions are treated as same.""" - - theta = Parameter("theta") - gate = RZGate(theta) - - refs = ParameterReferences(((gate, 0), (gate, 0))) - - self.assertIn((gate, 0), refs) - self.assertEqual(len(refs), 1) - self.assertIs(next(iter(refs))[0], gate) - self.assertEqual(next(iter(refs))[1], 0) - - def test_extend_refs(self): - """Extending references handles duplicates.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0,)) - refs |= ParameterReferences((ref0, ref1, ref2, ref1, ref0)) - - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - def test_copy_param_refs(self): - """Copy of parameter references is a shallow copy.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - ref3 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0, ref1)) - refs_copy = refs.copy() - - # Check same gate instances in copy - gate_ids = {id(ref0[0]), id(ref1[0])} - self.assertEqual({id(gate) for gate, _ in refs_copy}, gate_ids) - - # add new ref to original and check copy not modified - refs.add(ref2) - self.assertNotIn(ref2, refs_copy) - self.assertEqual(refs_copy, ParameterReferences((ref0, ref1))) - - # add new ref to copy and check original not modified - refs_copy.add(ref3) - self.assertNotIn(ref3, refs) - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - -class TestParameterTable(QiskitTestCase): - """Test the ParameterTable class.""" - - def test_init_param_table(self): - """Parameter table init from mapping.""" - - p1 = Parameter("theta") - p2 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p2), 0) - - mapping = {p1: ParameterReferences((ref0, ref1)), p2: ParameterReferences((ref2,))} - - table = ParameterTable(mapping) - - # make sure editing mapping doesn't change `table` - del mapping[p1] - - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - self.assertEqual(table[p2], ParameterReferences((ref2,))) - - def test_set_references(self): - """References replacement by parameter key.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - - table = ParameterTable() - table[p1] = ParameterReferences((ref0, ref1)) - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - - table[p1] = ParameterReferences((ref1,)) - self.assertEqual(table[p1], ParameterReferences((ref1,))) - - def test_set_references_from_iterable(self): - """Parameter table init from iterable.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p1), 0) - - table = ParameterTable({p1: ParameterReferences((ref0, ref1))}) - table[p1] = (ref2, ref1, ref0) - - self.assertEqual(table[p1], ParameterReferences((ref2, ref1, ref0))) - - class TestParameterView(QiskitTestCase): """Test the ParameterView object.""" diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py new file mode 100644 index 00000000000..06d4ed86a60 --- /dev/null +++ b/test/python/circuit/test_rust_equivalence.py @@ -0,0 +1,143 @@ +# 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. + +"""Rust gate definition tests""" + +from math import pi + +from test import QiskitTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping + +SKIP_LIST = {"cy", "ccx", "rx", "ry", "ecr", "sx"} +CUSTOM_MAPPING = {"x", "rz"} + + +class TestRustGateEquivalence(QiskitTestCase): + """Tests that compile time rust gate definitions is correct.""" + + def setUp(self): + super().setUp() + self.standard_gates = get_standard_gate_name_mapping() + # Pre-warm gate mapping cache, this is needed so rust -> py conversion is done + qc = QuantumCircuit(3) + for gate in self.standard_gates.values(): + if getattr(gate, "_standard_gate", None): + if gate.params: + gate = gate.base_class(*[pi] * len(gate.params)) + qc.append(gate, list(range(gate.num_qubits))) + + def test_definitions(self): + """Test definitions are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if name in SKIP_LIST: + # gate does not have a rust definition yet + continue + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [pi] * standard_gate._num_params() + py_def = gate_class.base_class(*params).definition + rs_def = standard_gate._get_definition(params) + if py_def is None: + self.assertIsNone(rs_def) + else: + rs_def = QuantumCircuit._from_circuit_data(rs_def) + for rs_inst, py_inst in zip(rs_def._data, py_def._data): + # Rust uses U but python still uses U3 and u2 + if rs_inst.operation.name == "u": + if py_inst.operation.name == "u3": + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + elif py_inst.operation.name == "u2": + self.assertEqual( + rs_inst.operation.params, + [ + pi / 2, + py_inst.operation.params[0], + py_inst.operation.params[1], + ], + ) + + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + # Rust uses P but python still uses u1 + elif rs_inst.operation.name == "p": + self.assertEqual(py_inst.operation.name, "u1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, rs_inst.operation.name) + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + + def test_matrix(self): + """Test matrices are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [pi] * standard_gate._num_params() + py_def = gate_class.base_class(*params).to_matrix() + rs_def = standard_gate._to_matrix(params) + np.testing.assert_allclose(rs_def, py_def) + + def test_name(self): + """Test that the gate name properties match in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.name, standard_gate.name) + + def test_num_qubits(self): + """Test the number of qubits are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.num_qubits, standard_gate.num_qubits) + + def test_num_params(self): + """Test the number of parameters are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual( + len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal" + ) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 598405beaae..135e874be48 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1946,7 +1946,7 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) - first_h = qc.h(1)[0].operation + qc.h(1) qc.cx(1, 2) qc.barrier() qc.cx(0, 1) @@ -1957,52 +1957,51 @@ def test_basis_gates(self): first_x = qc.x(2).c_if(qc.clbits[1], 1)[0].operation qc.z(2).c_if(qc.clbits[0], 1) - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - u3_2 = first_x.definition.data[0].operation - - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - "bit[2] c;", - "qubit[3] q;", - "h q[1];", - "cx q[1], q[2];", - "barrier q[0], q[1], q[2];", - "cx q[0], q[1];", - "h q[0];", - "barrier q[0], q[1], q[2];", - "c[0] = measure q[0];", - "c[1] = measure q[1];", - "barrier q[0], q[1], q[2];", - "if (c[1]) {", - " x q[2];", - "}", - "if (c[0]) {", - " z q[2];", - "}", - "", - ] - ) - self.assertEqual( - Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc), - expected_qasm, - ) + id_len = len(str(id(first_x))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "qubit[3] q;", + "h q[1];", + "cx q[1], q[2];", + "barrier q[0], q[1], q[2];", + "cx q[0], q[1];", + "h q[0];", + "barrier q[0], q[1], q[2];", + "c[0] = measure q[0];", + "c[1] = measure q[1];", + "barrier q[0], q[1], q[2];", + "if (c[1]) {", + " x q[2];", + "}", + "if (c[0]) {", + " z q[2];", + "}", + "", + ] + res = Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_teleportation(self): """Teleportation with physical qubits""" @@ -2120,62 +2119,58 @@ def test_no_include(self): circuit.sx(0) circuit.cx(0, 1) - rz = circuit.data[0].operation - u1_1 = rz.definition.data[0].operation - u3_1 = u1_1.definition.data[0].operation - sx = circuit.data[1].operation - sdg = sx.definition.data[0].operation - u1_2 = sdg.definition.data[0].operation - u3_2 = u1_2.definition.data[0].operation - h_ = sx.definition.data[1].operation - u2_1 = h_.definition.data[0].operation - u3_3 = u2_1.definition.data[0].operation - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_1)}(0, 0, pi/2) _gate_q_0;", - "}", - f"gate rz_{id(rz)}(_gate_p_0) _gate_q_0 {{", - f" u1_{id(u1_1)}(pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, -pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_2)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_2)}(0, 0, -pi/2) _gate_q_0;", - "}", - "gate sdg _gate_q_0 {", - f" u1_{id(u1_2)}(-pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2_1)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_3)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2_1)}(0, pi) _gate_q_0;", - "}", - "gate sx _gate_q_0 {", - " sdg _gate_q_0;", - " h _gate_q_0;", - " sdg _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "qubit[2] q;", - f"rz_{id(rz)}(pi/2) q[0];", - "sx q[0];", - "cx q[0], q[1];", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) + id_len = len(str(id(circuit.data[0].operation))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate rz_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u1_\d{%s}\(pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, -pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, -pi/2\) _gate_q_0;" % id_len), + "}", + "gate sdg _gate_q_0 {", + re.compile(r" u1_\d{%s}\(-pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate sx _gate_q_0 {", + " sdg _gate_q_0;", + " h _gate_q_0;", + " sdg _gate_q_0;", + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + "qubit[2] q;", + re.compile(r"rz_\d{%s}\(pi/2\) q\[0\];" % id_len), + "sx q[0];", + "cx q[0], q[1];", + "", + ] + res = Exporter(includes=[]).dumps(circuit).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_unusual_conditions(self): """Test that special QASM constructs such as ``measure`` are correctly handled when the From 2d6a5a2bbb3f330fe921120087358c3fd790d537 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 13 Jun 2024 13:11:26 +0200 Subject: [PATCH 116/159] exclude lines from coverage report (#12564) * exclude lines from coverage * comments with explanation * to pyproject from tox --- pyproject.toml | 9 +++++++++ qiskit/qasm3/exporter.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 149d6c0f2d4..0740861c98c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,3 +231,12 @@ enable = [ [tool.pylint.spelling] spelling-private-dict-file = ".local-spellings" + +[tool.coverage.report] +exclude_also = [ + "def __repr__", # Printable epresentational string does not typically execute during testing + "raise NotImplementedError", # Abstract methods are not testable + "raise RuntimeError", # Exceptions for defensive programming that cannot be tested a head + "if TYPE_CHECKING:", # Code that only runs during type checks + "@abstractmethod", # Abstract methods are not testable + ] diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 7f737af76eb..78b992b17cf 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -842,7 +842,7 @@ def build_current_scope(self) -> List[ast.Statement]: statements.append(self.build_if_statement(instruction)) elif isinstance(instruction.operation, SwitchCaseOp): statements.extend(self.build_switch_statement(instruction)) - else: # pragma: no cover + else: raise RuntimeError(f"unhandled control-flow construct: {instruction.operation}") continue # Build the node, ignoring any condition. @@ -1136,7 +1136,7 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType: return ast.BoolType() if type_.kind is types.Uint: return ast.UintType(type_.width) - raise RuntimeError(f"unhandled expr type '{type_}'") # pragma: no cover + raise RuntimeError(f"unhandled expr type '{type_}'") class _ExprBuilder(expr.ExprVisitor[ast.Expression]): From 81433d53058a207ba66a300180664cfdfb1725fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:20:17 +0000 Subject: [PATCH 117/159] Bump pypa/cibuildwheel in the github_actions group across 1 directory (#12568) Bumps the github_actions group with 1 update in the / directory: [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel). Updates `pypa/cibuildwheel` from 2.17.0 to 2.19.1 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.17.0...v2.19.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2cd3c8ac0a3..7c29f1376e4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,7 @@ jobs: with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: 'bash ./tools/build_pgo.sh /tmp/pgo-data/merged.profdata' CIBW_BEFORE_BUILD_WINDOWS: 'bash ./tools/build_pgo.sh /tmp/pgo-data/merged.profdata && cp /tmp/pgo-data/merged.profdata ~/.' @@ -58,7 +58,7 @@ jobs: with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin CIBW_BUILD: cp38-macosx_universal2 cp38-macosx_arm64 @@ -87,7 +87,7 @@ jobs: with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' - uses: actions/upload-artifact@v4 @@ -133,7 +133,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: s390x CIBW_TEST_SKIP: "cp*" @@ -167,7 +167,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: ppc64le CIBW_TEST_SKIP: "cp*" @@ -201,7 +201,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: aarch64 - uses: actions/upload-artifact@v4 From cd2c65d773895a2e8a57568ad490463ccd0122c6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 14 Jun 2024 11:32:03 +0200 Subject: [PATCH 118/159] Bump version of macOS used on Azure (#12571) The macOS 11 runners are deprecated pending removal. While macOS 14 is available, it's still marked as in preview on Azure, so macOS 13 is the current "latest" stable version. --- .azure/test-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 1887a88655d..b167df3f1c8 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -10,7 +10,7 @@ parameters: jobs: - job: "MacOS_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test macOS Python ${{ parameters.pythonVersion }}" - pool: {vmImage: 'macOS-11'} + pool: {vmImage: 'macOS-13'} variables: QISKIT_SUPPRESS_PACKAGING_WARNINGS: Y From 17ab36478689200f6a661f731d13c24473031a30 Mon Sep 17 00:00:00 2001 From: Catherine Lozano Date: Mon, 17 Jun 2024 09:52:53 -0400 Subject: [PATCH 119/159] += depreciated changed to &= (#12515) * += depreciated * fixed deprecated += to &= in two_local --- qiskit/circuit/library/n_local/two_local.py | 2 +- test/benchmarks/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index 61b9e725cc6..1cb388349fe 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -87,7 +87,7 @@ class TwoLocal(NLocal): >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True) >>> qc = QuantumCircuit(3) - >>> qc += two + >>> qc &= two >>> print(qc.decompose().draw()) ┌──────────┐┌──────────┐ ░ ░ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├─░──■──■─────░─┤ Ry(θ[6]) ├─┤ Rz(θ[9]) ├ diff --git a/test/benchmarks/utils.py b/test/benchmarks/utils.py index bbd7d0a9af8..13350346b82 100644 --- a/test/benchmarks/utils.py +++ b/test/benchmarks/utils.py @@ -215,7 +215,7 @@ def unmajority(p, a, b, c): qc.x(a[0]) # Set input a = 0...0001 qc.x(b) # Set input b = 1...1111 # Apply the adder - qc += adder_subcircuit + qc &= adder_subcircuit # Measure the output register in the computational basis for j in range(n): From 0bca3c403bb20ada4fef8f17d0affcf93ebcbefb Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 17 Jun 2024 17:20:58 +0200 Subject: [PATCH 120/159] Remove extra parenthesis in Primitive examples (#12587) * remove extra parenthesis * Update qiskit/primitives/__init__.py --------- Co-authored-by: Julien Gacon --- qiskit/primitives/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 569c075b23b..7cf8355354e 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -51,7 +51,7 @@ * a collection parameter value sets to bind the circuit against, :math:`\theta_k`. Running an estimator returns a :class:`~qiskit.primitives.BasePrimitiveJob` object, where calling -the method :meth:`~qiskit.primitives.BasePrimitiveJob.result` results in expectation value estimates +the method :meth:`~qiskit.primitives.BasePrimitiveJob.result` results in expectation value estimates and metadata for each pub: .. math:: @@ -95,7 +95,7 @@ # [] ] job2 = estimator.run( [ - (psi1, [H1, H3], [theta1, theta3]), + (psi1, [H1, H3], [theta1, theta3]), (psi2, H2, theta2) ], precision=0.01 @@ -103,7 +103,7 @@ job_result = job2.result() print(f"The primitive-job finished with result {job_result}") - + Overview of SamplerV2 ===================== @@ -153,12 +153,12 @@ # collect 128 shots from the Bell circuit job = sampler.run([bell], shots=128) job_result = job.result() - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # run a sampler job on the parameterized circuits - job2 = sampler.run([(pqc, theta1), (pqc2, theta2)] + job2 = sampler.run([(pqc, theta1), (pqc2, theta2)]) job_result = job2.result() - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") Overview of EstimatorV1 @@ -214,14 +214,14 @@ # calculate [ ] job = estimator.run([psi1], [H1], [theta1]) job_result = job.result() # It will block until the job finishes. - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # calculate [ , # , # ] job2 = estimator.run( - [psi1, psi2, psi1], - [H1, H2, H3], + [psi1, psi2, psi1], + [H1, H2, H3], [theta1, theta2, theta3] ) job_result = job2.result() From 864a2a3c68d6901ac0209b0a5fd02344430c79d3 Mon Sep 17 00:00:00 2001 From: Catherine Lozano Date: Mon, 17 Jun 2024 11:59:02 -0400 Subject: [PATCH 121/159] Fixed incorrect behavior of scheduling benchmarks (#12524) * Setting conditional and reset to false as not supported in alap/asap * removed deprecated test * removed unused dag --- test/benchmarks/scheduling_passes.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/test/benchmarks/scheduling_passes.py b/test/benchmarks/scheduling_passes.py index a4c25dc46bc..34d40ea97e6 100644 --- a/test/benchmarks/scheduling_passes.py +++ b/test/benchmarks/scheduling_passes.py @@ -37,7 +37,7 @@ class SchedulingPassBenchmarks: def setup(self, n_qubits, depth): seed = 42 self.circuit = random_circuit( - n_qubits, depth, measure=True, conditional=True, reset=True, seed=seed, max_operands=2 + n_qubits, depth, measure=True, conditional=False, reset=False, seed=seed, max_operands=2 ) self.basis_gates = ["rz", "sx", "x", "cx", "id", "reset"] self.cmap = [ @@ -108,15 +108,6 @@ def setup(self, n_qubits, depth): ], dt=1e-9, ) - self.timed_dag = TimeUnitConversion(self.durations).run(self.dag) - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(self.durations), - PadDynamicalDecoupling(self.durations, dd_sequence), - ] - ) - self.scheduled_dag = pm.run(self.timed_dag) def time_time_unit_conversion_pass(self, _, __): TimeUnitConversion(self.durations).run(self.dag) @@ -129,7 +120,7 @@ def time_alap_schedule_pass(self, _, __): PadDynamicalDecoupling(self.durations, dd_sequence), ] ) - pm.run(self.timed_dag) + pm.run(self.transpiled_circuit) def time_asap_schedule_pass(self, _, __): dd_sequence = [XGate(), XGate()] @@ -139,9 +130,4 @@ def time_asap_schedule_pass(self, _, __): PadDynamicalDecoupling(self.durations, dd_sequence), ] ) - pm.run(self.timed_dag) - - def time_dynamical_decoupling_pass(self, _, __): - PadDynamicalDecoupling(self.durations, dd_sequence=[XGate(), XGate()]).run( - self.scheduled_dag - ) + pm.run(self.transpiled_circuit) From 6d6dce327264201d7158baf6af2152bbe387fc57 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 18 Jun 2024 05:12:04 -0400 Subject: [PATCH 122/159] Remove Eric from Rust bot notifications (#12596) --- qiskit_bot.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 2467665e0d0..edff5997c8f 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -28,7 +28,6 @@ notifications: ".*\\.rs$|^Cargo": - "`@mtreinish`" - "`@kevinhartman`" - - "@Eric-Arellano" "(?!.*pulse.*)\\bvisualization\\b": - "@enavarro51" "^docs/": From d4e795b43146b01103df608d1cf55425b4bfd765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:28:59 +0200 Subject: [PATCH 123/159] Add rust representation for SGates, TGates, and iSwap gate (#12598) * Add SGate, SdgGate, iSWAP, TGate, and TdgGate to standard gates in rust. Add missing gate definitions that depended on these gates (marked as todo). * Add fast path to circuit methods, fix sneaky bugs, unskip cy and sx tests. * Unskip ccx test too! * Fix black --- crates/circuit/src/gate_matrix.rs | 24 ++ crates/circuit/src/imports.rs | 14 +- crates/circuit/src/operations.rs | 220 ++++++++++++++++-- .../circuit/library/standard_gates/iswap.py | 3 + qiskit/circuit/library/standard_gates/s.py | 5 + qiskit/circuit/library/standard_gates/sx.py | 2 + qiskit/circuit/library/standard_gates/t.py | 5 + qiskit/circuit/quantumcircuit.py | 24 +- test/python/circuit/test_rust_equivalence.py | 2 +- 9 files changed, 265 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 72e1087637c..ad8c918e73b 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -63,6 +63,11 @@ pub static SX_GATE: [[Complex64; 2]; 2] = [ [c64(0.5, -0.5), c64(0.5, 0.5)], ]; +pub static SXDG_GATE: [[Complex64; 2]; 2] = [ + [c64(0.5, -0.5), c64(0.5, 0.5)], + [c64(0.5, 0.5), c64(0.5, -0.5)], +]; + pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; @@ -199,6 +204,25 @@ pub static SWAP_GATE: [[Complex64; 4]; 4] = [ [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], ]; +pub static ISWAP_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 1.), c64(0., 0.)], + [c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], +]; + +pub static S_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., 1.)]]; +pub static SDG_GATE: [[Complex64; 2]; 2] = + [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., -1.)]]; + +pub static T_GATE: [[Complex64; 2]; 2] = [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)], +]; +pub static TDG_GATE: [[Complex64; 2]; 2] = [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], +]; #[inline] pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 050f7f2e053..8db3b88fd7d 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -77,7 +77,7 @@ pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = /// when a gate is added directly via the StandardGate path and there isn't a Python object /// to poll the _standard_gate attribute for. /// -/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// NOTE: the order here is significant, the StandardGate variant's number must match /// index of it's entry in this table. This is all done statically for performance static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // ZGate = 0 @@ -119,6 +119,18 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ ["qiskit.circuit.library.standard_gates.p", "PhaseGate"], // UGate = 17 ["qiskit.circuit.library.standard_gates.u", "UGate"], + // SGate = 18 + ["qiskit.circuit.library.standard_gates.s", "SGate"], + // SdgGate = 19 + ["qiskit.circuit.library.standard_gates.s", "SdgGate"], + // TGate = 20 + ["qiskit.circuit.library.standard_gates.s", "TGate"], + // TdgGate = 21 + ["qiskit.circuit.library.standard_gates.s", "TdgGate"], + // SXdgGate = 22 + ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], + // iSWAPGate = 23 + ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index ead1b8ee1eb..9048c55d9d4 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -24,6 +24,9 @@ use pyo3::prelude::*; use pyo3::{intern, IntoPy, Python}; use smallvec::smallvec; +const PI2: f64 = PI / 2.0; +const PI4: f64 = PI / 4.0; + /// Valid types for an operation field in a CircuitInstruction /// /// These are basically the types allowed in a QuantumCircuit @@ -194,13 +197,21 @@ pub enum StandardGate { HGate = 15, PhaseGate = 16, UGate = 17, + SGate = 18, + SdgGate = 19, + TGate = 20, + TdgGate = 21, + SXdgGate = 22, + ISwapGate = 23, } -static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = - [1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1]; +static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, +]; -static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = - [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3]; +static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, +]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "z", @@ -221,6 +232,12 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "h", "p", "u", + "s", + "sdg", + "t", + "tdg", + "sxdg", + "iswap", ]; #[pymethods] @@ -269,7 +286,8 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 18; + +pub const STANDARD_GATE_SIZE: usize = 24; impl Operation for StandardGate { fn name(&self) -> &str { @@ -350,6 +368,10 @@ impl Operation for StandardGate { [] => Some(aview2(&gate_matrix::SX_GATE).to_owned()), _ => None, }, + Self::SXdgGate => match params { + [] => Some(aview2(&gate_matrix::SXDG_GATE).to_owned()), + _ => None, + }, Self::GlobalPhaseGate => match params { [Param::Float(theta)] => { Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) @@ -374,6 +396,26 @@ impl Operation for StandardGate { } _ => None, }, + Self::SGate => match params { + [] => Some(aview2(&gate_matrix::S_GATE).to_owned()), + _ => None, + }, + Self::SdgGate => match params { + [] => Some(aview2(&gate_matrix::SDG_GATE).to_owned()), + _ => None, + }, + Self::TGate => match params { + [] => Some(aview2(&gate_matrix::T_GATE).to_owned()), + _ => None, + }, + Self::TdgGate => match params { + [] => Some(aview2(&gate_matrix::TDG_GATE).to_owned()), + _ => None, + }, + Self::ISwapGate => match params { + [] => Some(aview2(&gate_matrix::ISWAP_GATE).to_owned()), + _ => None, + }, } } @@ -401,11 +443,7 @@ impl Operation for StandardGate { 1, [( Self::UGate, - smallvec![ - Param::Float(PI), - Param::Float(PI / 2.), - Param::Float(PI / 2.), - ], + smallvec![Param::Float(PI), Param::Float(PI2), Param::Float(PI2),], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -445,9 +483,56 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CYGate => todo!("Add when we have S and S dagger"), + Self::CYGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SdgGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::SGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::CXGate => None, - Self::CCXGate => todo!("Add when we have T and TDagger"), + Self::CCXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q2 = smallvec![Qubit(2)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + let q0_2 = smallvec![Qubit(0), Qubit(2)]; + let q1_2 = smallvec![Qubit(1), Qubit(2)]; + Some( + CircuitData::from_standard_gates( + py, + 3, + [ + (Self::HGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q1_2.clone()), + (Self::TdgGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q0_2.clone()), + (Self::TGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q1_2), + (Self::TdgGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q0_2), + (Self::TGate, smallvec![], q1.clone()), + (Self::TGate, smallvec![], q2.clone()), + (Self::HGate, smallvec![], q2), + (Self::CXGate, smallvec![], q0_1.clone()), + (Self::TGate, smallvec![], smallvec![Qubit(0)]), + (Self::TdgGate, smallvec![], q1), + (Self::CXGate, smallvec![], q0_1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::RXGate => todo!("Add when we have R"), Self::RYGate => todo!("Add when we have R"), Self::RZGate => Python::with_gil(|py| -> Option { @@ -501,7 +586,36 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::SXGate => todo!("Add when we have S dagger"), + Self::SXGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [ + (Self::SdgGate, smallvec![], smallvec![Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::SdgGate, smallvec![], smallvec![Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SXdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [ + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates(py, 0, [], params[0].clone()) @@ -516,7 +630,7 @@ impl Operation for StandardGate { 1, [( Self::UGate, - smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], + smallvec![Param::Float(PI2), Param::Float(0.), Param::Float(PI)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -540,6 +654,84 @@ impl Operation for StandardGate { ) }), Self::UGate => None, + Self::SGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI2)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(-PI2)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::TGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI4)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::TdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(-PI4)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::ISwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + (Self::SGate, smallvec![], smallvec![Qubit(1)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), } } diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index 50d3a6bb347..8074990a384 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate from .xx_plus_yy import XXPlusYYGate @@ -85,6 +86,8 @@ class iSwapGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.ISwapGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new iSwap gate.""" super().__init__("iswap", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 6fde1c6544e..f62d16a10d4 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -20,6 +20,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _S_ARRAY = numpy.array([[1, 0], [0, 1j]]) @@ -57,6 +58,8 @@ class SGate(SingletonGate): Equivalent to a :math:`\pi/2` radian rotation about the Z axis. """ + _standard_gate = StandardGate.SGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new S gate.""" super().__init__("s", 1, [], label=label, duration=duration, unit=unit) @@ -134,6 +137,8 @@ class SdgGate(SingletonGate): Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ + _standard_gate = StandardGate.SdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Sdg gate.""" super().__init__("sdg", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 93ca85da019..72e4a8f9b5b 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -167,6 +167,8 @@ class SXdgGate(SingletonGate): = e^{-i \pi/4} \sqrt{X}^{\dagger} """ + _standard_gate = StandardGate.SXdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SXdg gate.""" super().__init__("sxdg", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 87a38d9d44c..e4301168ac5 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -21,6 +21,7 @@ from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) @@ -55,6 +56,8 @@ class TGate(SingletonGate): Equivalent to a :math:`\pi/4` radian rotation about the Z axis. """ + _standard_gate = StandardGate.TGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new T gate.""" super().__init__("t", 1, [], label=label, duration=duration, unit=unit) @@ -130,6 +133,8 @@ class TdgGate(SingletonGate): Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ + _standard_gate = StandardGate.TdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Tdg gate.""" super().__init__("tdg", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 238a2682522..e3fbf40da68 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4979,9 +4979,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.s import SGate - - return self.append(SGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SGate, [], [qubit], cargs=None) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -4994,9 +4992,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.s import SdgGate - - return self.append(SdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SdgGate, [], [qubit], cargs=None) def cs( self, @@ -5089,9 +5085,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe Returns: A handle to the instructions created. """ - from .library.standard_gates.iswap import iSwapGate - - return self.append(iSwapGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.ISwapGate, [], [qubit1, qubit2], cargs=None) def cswap( self, @@ -5150,9 +5144,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.sx import SXdgGate - - return self.append(SXdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SXdgGate, None, qargs=[qubit]) def csx( self, @@ -5196,9 +5188,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.t import TGate - - return self.append(TGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.TGate, [], [qubit], cargs=None) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -5211,9 +5201,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.t import TdgGate - - return self.append(TdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.TdgGate, [], [qubit], cargs=None) def u( self, diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index 06d4ed86a60..bb09ae4caf3 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -21,7 +21,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -SKIP_LIST = {"cy", "ccx", "rx", "ry", "ecr", "sx"} +SKIP_LIST = {"rx", "ry", "ecr"} CUSTOM_MAPPING = {"x", "rz"} From 53667d167e2de2f841d3b781877427f0b459289b Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Wed, 19 Jun 2024 03:05:56 -0400 Subject: [PATCH 124/159] Remove consider-using-f-string lint rule and updates (#12423) * remove consider-using-f-string lint rule and updates * reverting a latex update * f-string update based on review * Update qiskit/circuit/library/hamiltonian_gate.py Co-authored-by: Matthew Treinish * Update qiskit/circuit/tools/pi_check.py Co-authored-by: Matthew Treinish * Update qiskit/circuit/tools/pi_check.py Co-authored-by: Matthew Treinish * updates after merge * Update qiskit/providers/models/backendproperties.py Co-authored-by: Julien Gacon * Update qiskit/synthesis/linear/cnot_synth.py Co-authored-by: Julien Gacon * updates from PR * latex fixes --------- Co-authored-by: Matthew Treinish Co-authored-by: Julien Gacon --- pyproject.toml | 1 - qiskit/assembler/assemble_circuits.py | 4 +- qiskit/assembler/assemble_schedules.py | 17 ++---- qiskit/assembler/disassemble.py | 2 +- .../classicalfunction/boolean_expression.py | 2 +- .../classical_function_visitor.py | 10 ++-- qiskit/circuit/classicalfunction/utils.py | 2 +- qiskit/circuit/classicalregister.py | 2 +- qiskit/circuit/delay.py | 2 +- qiskit/circuit/duration.py | 4 +- qiskit/circuit/equivalence.py | 12 ++-- qiskit/circuit/gate.py | 4 +- qiskit/circuit/instruction.py | 9 +-- .../arithmetic/linear_pauli_rotations.py | 2 +- .../library/arithmetic/piecewise_chebyshev.py | 2 +- .../piecewise_linear_pauli_rotations.py | 2 +- .../piecewise_polynomial_pauli_rotations.py | 2 +- .../arithmetic/polynomial_pauli_rotations.py | 2 +- .../data_preparation/state_preparation.py | 14 ++--- qiskit/circuit/library/graph_state.py | 2 +- qiskit/circuit/library/hamiltonian_gate.py | 3 +- .../circuit/library/hidden_linear_function.py | 2 +- qiskit/circuit/library/n_local/n_local.py | 5 +- qiskit/circuit/library/n_local/qaoa_ansatz.py | 14 ++--- qiskit/circuit/library/overlap.py | 6 +- qiskit/circuit/library/standard_gates/u3.py | 2 +- qiskit/circuit/parameter.py | 4 +- qiskit/circuit/parameterexpression.py | 18 +++--- qiskit/circuit/quantumcircuit.py | 13 +++-- qiskit/circuit/quantumregister.py | 2 +- qiskit/circuit/register.py | 12 ++-- qiskit/circuit/tools/pi_check.py | 8 +-- qiskit/compiler/assembler.py | 24 ++++---- qiskit/compiler/scheduler.py | 2 +- qiskit/compiler/transpiler.py | 4 +- qiskit/converters/circuit_to_gate.py | 12 ++-- qiskit/converters/circuit_to_instruction.py | 6 +- qiskit/dagcircuit/dagcircuit.py | 57 +++++++++---------- qiskit/dagcircuit/dagdependency.py | 8 +-- qiskit/dagcircuit/dagdependency_v2.py | 8 +-- qiskit/dagcircuit/dagdepnode.py | 2 +- qiskit/passmanager/flow_controllers.py | 2 +- qiskit/passmanager/passmanager.py | 2 +- qiskit/providers/backend.py | 8 +-- .../basic_provider/basic_provider_tools.py | 2 +- .../basic_provider/basic_simulator.py | 4 +- .../providers/fake_provider/fake_backend.py | 4 +- .../fake_provider/generic_backend_v2.py | 4 +- qiskit/providers/models/backendproperties.py | 4 +- qiskit/providers/models/pulsedefaults.py | 7 +-- qiskit/providers/options.py | 4 +- qiskit/pulse/configuration.py | 20 +++---- qiskit/pulse/instruction_schedule_map.py | 6 +- qiskit/pulse/instructions/acquire.py | 15 +++-- qiskit/pulse/instructions/instruction.py | 5 +- qiskit/pulse/library/samplers/decorators.py | 14 ++--- qiskit/pulse/library/symbolic_pulses.py | 7 +-- qiskit/pulse/library/waveform.py | 7 +-- qiskit/pulse/macros.py | 8 +-- qiskit/pulse/parser.py | 14 ++--- qiskit/pulse/schedule.py | 26 +++------ qiskit/pulse/transforms/alignments.py | 4 +- qiskit/pulse/utils.py | 3 +- qiskit/qasm2/export.py | 12 ++-- qiskit/qobj/converters/pulse_instruction.py | 6 +- qiskit/qobj/pulse_qobj.py | 27 ++++----- qiskit/qobj/qasm_qobj.py | 35 +++++------- qiskit/qpy/binary_io/circuits.py | 10 ++-- qiskit/qpy/binary_io/value.py | 6 +- qiskit/qpy/interface.py | 5 +- .../operators/channel/quantum_channel.py | 9 +-- .../quantum_info/operators/channel/superop.py | 4 +- .../operators/dihedral/dihedral_circuits.py | 4 +- qiskit/quantum_info/operators/measures.py | 2 +- qiskit/quantum_info/operators/op_shape.py | 32 ++++------- qiskit/quantum_info/operators/operator.py | 15 ++--- qiskit/quantum_info/operators/predicates.py | 1 + .../operators/symplectic/base_pauli.py | 18 +++--- .../operators/symplectic/pauli.py | 6 +- .../operators/symplectic/pauli_list.py | 17 +++--- .../operators/symplectic/sparse_pauli_op.py | 10 ++-- qiskit/quantum_info/states/densitymatrix.py | 13 ++--- qiskit/quantum_info/states/statevector.py | 12 ++-- qiskit/result/counts.py | 2 +- .../correlated_readout_mitigator.py | 4 +- .../mitigation/local_readout_mitigator.py | 4 +- qiskit/result/mitigation/utils.py | 4 +- qiskit/result/models.py | 21 +++---- qiskit/result/result.py | 25 +++----- qiskit/scheduler/lowering.py | 4 +- qiskit/synthesis/linear/cnot_synth.py | 3 +- .../two_qubit/two_qubit_decompose.py | 2 +- qiskit/transpiler/coupling.py | 6 +- qiskit/transpiler/layout.py | 6 +- .../passes/basis/basis_translator.py | 4 +- .../passes/basis/unroll_3q_or_more.py | 2 +- .../passes/basis/unroll_custom_definitions.py | 6 +- .../passes/calibration/rzx_builder.py | 6 +- .../optimization/inverse_cancellation.py | 4 +- .../passes/optimization/optimize_1q_gates.py | 2 +- .../transpiler/passes/routing/sabre_swap.py | 2 +- .../passes/routing/stochastic_swap.py | 4 +- .../passes/synthesis/high_level_synthesis.py | 4 +- qiskit/transpiler/passes/utils/check_map.py | 6 +- qiskit/transpiler/passes/utils/error.py | 4 +- qiskit/transpiler/passes/utils/fixed_point.py | 6 +- .../transpiler/preset_passmanagers/common.py | 4 +- qiskit/transpiler/target.py | 4 +- qiskit/user_config.py | 12 ++-- .../circuit/circuit_visualization.py | 4 +- qiskit/visualization/circuit/latex.py | 33 +++++------ qiskit/visualization/circuit/matplotlib.py | 2 +- qiskit/visualization/circuit/text.py | 13 +++-- qiskit/visualization/dag_visualization.py | 4 +- qiskit/visualization/pulse_v2/core.py | 2 +- .../pulse_v2/generators/frame.py | 7 +-- .../pulse_v2/generators/waveform.py | 14 ++--- qiskit/visualization/pulse_v2/layouts.py | 6 +- .../pulse_v2/plotters/matplotlib.py | 3 +- qiskit/visualization/state_visualization.py | 11 ++-- .../timeline/plotters/matplotlib.py | 3 +- test/benchmarks/circuit_construction.py | 2 +- test/python/circuit/test_circuit_qasm.py | 2 +- test/python/circuit/test_circuit_registers.py | 2 +- test/python/circuit/test_instructions.py | 10 ++-- test/python/circuit/test_parameters.py | 15 +++-- test/python/dagcircuit/test_dagcircuit.py | 8 +-- test/python/providers/test_fake_backends.py | 7 +-- .../operators/symplectic/test_clifford.py | 10 ++-- .../quantum_info/states/test_densitymatrix.py | 4 +- test/python/result/test_mitigators.py | 54 +++++++----------- .../aqc/fast_gradient/test_layer1q.py | 9 ++- .../aqc/fast_gradient/test_layer2q.py | 10 ++-- .../synthesis/test_permutation_synthesis.py | 8 +-- test/python/test_user_config.py | 2 +- test/python/transpiler/test_pass_scheduler.py | 2 +- .../visualization/timeline/test_generators.py | 11 ++-- .../randomized/test_transpiler_equivalence.py | 7 +-- test/utils/base.py | 12 ++-- test/visual/results.py | 31 +++++----- tools/build_standard_commutations.py | 4 +- tools/find_stray_release_notes.py | 2 +- tools/verify_headers.py | 12 ++-- 143 files changed, 516 insertions(+), 684 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0740861c98c..2f62557aa15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,7 +218,6 @@ disable = [ # TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale - "consider-using-f-string", "no-member", # for dynamically created members "not-context-manager", "unnecessary-lambda-assignment", # do not want to implement diff --git a/qiskit/assembler/assemble_circuits.py b/qiskit/assembler/assemble_circuits.py index b27fe47a02e..a3d9b6bbb54 100644 --- a/qiskit/assembler/assemble_circuits.py +++ b/qiskit/assembler/assemble_circuits.py @@ -153,9 +153,9 @@ def _assemble_circuit( conditional_reg_idx = memory_slots + max_conditional_idx conversion_bfunc = QasmQobjInstruction( name="bfunc", - mask="0x%X" % mask, + mask="0x%X" % mask, # pylint: disable=consider-using-f-string relation="==", - val="0x%X" % val, + val="0x%X" % val, # pylint: disable=consider-using-f-string register=conditional_reg_idx, ) instructions.append(conversion_bfunc) diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index c60c28ff9a5..2d5ebefa2fd 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -152,7 +152,7 @@ def _assemble_experiments( # TODO: add other experimental header items (see circuit assembler) qobj_experiment_header = qobj.QobjExperimentHeader( memory_slots=max_memory_slot + 1, # Memory slots are 0 indexed - name=sched.name or "Experiment-%d" % idx, + name=sched.name or f"Experiment-{idx}", metadata=metadata, ) @@ -306,18 +306,11 @@ def _validate_meas_map( common_next = next_inst_qubits.intersection(meas_set) if common_instr_qubits and common_next: raise QiskitError( - "Qubits {} and {} are in the same measurement grouping: {}. " + f"Qubits {common_instr_qubits} and {common_next} are in the same measurement " + f"grouping: {meas_map}. " "They must either be acquired at the same time, or disjointly" - ". Instead, they were acquired at times: {}-{} and " - "{}-{}".format( - common_instr_qubits, - common_next, - meas_map, - inst[0][0], - inst_end_time, - next_inst_time, - next_inst_time + next_inst[0][1], - ) + f". Instead, they were acquired at times: {inst[0][0]}-{inst_end_time} and " + f"{next_inst_time}-{next_inst_time + next_inst[0][1]}" ) diff --git a/qiskit/assembler/disassemble.py b/qiskit/assembler/disassemble.py index c94b108c4b2..127bbd35eb2 100644 --- a/qiskit/assembler/disassemble.py +++ b/qiskit/assembler/disassemble.py @@ -109,7 +109,7 @@ def _qobj_to_circuit_cals(qobj, pulse_lib): config = (tuple(gate["qubits"]), tuple(gate["params"])) cal = { config: pulse.Schedule( - name="{} {} {}".format(gate["name"], str(gate["params"]), str(gate["qubits"])) + name=f"{gate['name']} {str(gate['params'])} {str(gate['qubits'])}" ) } for instruction in gate["instructions"]: diff --git a/qiskit/circuit/classicalfunction/boolean_expression.py b/qiskit/circuit/classicalfunction/boolean_expression.py index 0f4a53494af..e517f36db02 100644 --- a/qiskit/circuit/classicalfunction/boolean_expression.py +++ b/qiskit/circuit/classicalfunction/boolean_expression.py @@ -116,7 +116,7 @@ def from_dimacs_file(cls, filename: str): expr_obj = cls.__new__(cls) if not isfile(filename): - raise FileNotFoundError("The file %s does not exists." % filename) + raise FileNotFoundError(f"The file {filename} does not exists.") expr_obj._tweedledum_bool_expression = BoolFunction.from_dimacs_file(filename) num_qubits = ( diff --git a/qiskit/circuit/classicalfunction/classical_function_visitor.py b/qiskit/circuit/classicalfunction/classical_function_visitor.py index dfe8b956b09..be89e8ee7f8 100644 --- a/qiskit/circuit/classicalfunction/classical_function_visitor.py +++ b/qiskit/circuit/classicalfunction/classical_function_visitor.py @@ -83,7 +83,7 @@ def bit_binop(self, op, values): """Uses ClassicalFunctionVisitor.bitops to extend self._network""" bitop = ClassicalFunctionVisitor.bitops.get(type(op)) if not bitop: - raise ClassicalFunctionParseError("Unknown binop.op %s" % op) + raise ClassicalFunctionParseError(f"Unknown binop.op {op}") binop = getattr(self._network, bitop) left_type, left_signal = values[0] @@ -112,19 +112,19 @@ def visit_UnaryOp(self, node): operand_type, operand_signal = self.visit(node.operand) if operand_type != "Int1": raise ClassicalFunctionCompilerTypeError( - "UntaryOp.op %s only support operation on Int1s for now" % node.op + f"UntaryOp.op {node.op} only support operation on Int1s for now" ) bitop = ClassicalFunctionVisitor.bitops.get(type(node.op)) if not bitop: raise ClassicalFunctionCompilerTypeError( - "UntaryOp.op %s does not operate with Int1 type " % node.op + f"UntaryOp.op {node.op} does not operate with Int1 type " ) return "Int1", getattr(self._network, bitop)(operand_signal) def visit_Name(self, node): """Reduce variable names.""" if node.id not in self.scopes[-1]: - raise ClassicalFunctionParseError("out of scope: %s" % node.id) + raise ClassicalFunctionParseError(f"out of scope: {node.id}") return self.scopes[-1][node.id] def generic_visit(self, node): @@ -143,7 +143,7 @@ def generic_visit(self, node): ), ): return super().generic_visit(node) - raise ClassicalFunctionParseError("Unknown node: %s" % type(node)) + raise ClassicalFunctionParseError(f"Unknown node: {type(node)}") def extend_scope(self, args_node: _ast.arguments) -> None: """Add the arguments to the scope""" diff --git a/qiskit/circuit/classicalfunction/utils.py b/qiskit/circuit/classicalfunction/utils.py index 237a8b83853..75dcd3e20a7 100644 --- a/qiskit/circuit/classicalfunction/utils.py +++ b/qiskit/circuit/classicalfunction/utils.py @@ -47,7 +47,7 @@ def _convert_tweedledum_operator(op): if op.kind() == "py_operator": return op.py_op() else: - raise RuntimeError("Unrecognized operator: %s" % op.kind()) + raise RuntimeError(f"Unrecognized operator: {op.kind()}") # TODO: need to deal with cbits too! if op.num_controls() > 0: diff --git a/qiskit/circuit/classicalregister.py b/qiskit/circuit/classicalregister.py index 7a21e6b2fa5..802d8c602e2 100644 --- a/qiskit/circuit/classicalregister.py +++ b/qiskit/circuit/classicalregister.py @@ -43,7 +43,7 @@ def __init__(self, register=None, index=None): super().__init__(register, index) else: raise CircuitError( - "Clbit needs a ClassicalRegister and %s was provided" % type(register).__name__ + f"Clbit needs a ClassicalRegister and {type(register).__name__} was provided" ) diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index a333125a5a2..25e7a6f3356 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -32,7 +32,7 @@ def __init__(self, duration, unit="dt"): unit: the unit of the duration. Must be ``"dt"`` or an SI-prefixed seconds unit. """ if unit not in {"s", "ms", "us", "ns", "ps", "dt"}: - raise CircuitError("Unknown unit %s is specified." % unit) + raise CircuitError(f"Unknown unit {unit} is specified.") super().__init__("delay", 1, 0, params=[duration], unit=unit) diff --git a/qiskit/circuit/duration.py b/qiskit/circuit/duration.py index 6acb230baad..fdf6e99e611 100644 --- a/qiskit/circuit/duration.py +++ b/qiskit/circuit/duration.py @@ -35,8 +35,8 @@ def duration_in_dt(duration_in_sec: float, dt_in_sec: float) -> int: rounding_error = abs(duration_in_sec - res * dt_in_sec) if rounding_error > 1e-15: warnings.warn( - "Duration is rounded to %d [dt] = %e [s] from %e [s]" - % (res, res * dt_in_sec, duration_in_sec), + f"Duration is rounded to {res:d} [dt] = {res * dt_in_sec:e} [s] " + f"from {duration_in_sec:e} [s]", UserWarning, ) return res diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 45921c3f229..17912517d24 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -249,7 +249,7 @@ def _build_basis_graph(self): ) node_map[decomp_basis] = decomp_basis_node - label = "{}\n{}".format(str(params), str(decomp) if num_qubits <= 5 else "...") + label = f"{str(params)}\n{str(decomp) if num_qubits <= 5 else '...'}" graph.add_edge( node_map[basis], node_map[decomp_basis], @@ -273,8 +273,8 @@ def _raise_if_param_mismatch(gate_params, circuit_parameters): if set(gate_parameters) != circuit_parameters: raise CircuitError( "Cannot add equivalence between circuit and gate " - "of different parameters. Gate params: {}. " - "Circuit params: {}.".format(gate_parameters, circuit_parameters) + f"of different parameters. Gate params: {gate_parameters}. " + f"Circuit params: {circuit_parameters}." ) @@ -282,10 +282,8 @@ def _raise_if_shape_mismatch(gate, circuit): if gate.num_qubits != circuit.num_qubits or gate.num_clbits != circuit.num_clbits: raise CircuitError( "Cannot add equivalence between circuit and gate " - "of different shapes. Gate: {} qubits and {} clbits. " - "Circuit: {} qubits and {} clbits.".format( - gate.num_qubits, gate.num_clbits, circuit.num_qubits, circuit.num_clbits - ) + f"of different shapes. Gate: {gate.num_qubits} qubits and {gate.num_clbits} clbits. " + f"Circuit: {circuit.num_qubits} qubits and {circuit.num_clbits} clbits." ) diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 13252677586..d2c88f40bdb 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -177,7 +177,7 @@ def _broadcast_3_or_more_args(qargs: list) -> Iterator[tuple[list, list]]: for arg in zip(*qargs): yield list(arg), [] else: - raise CircuitError("Not sure how to combine these qubit arguments:\n %s\n" % qargs) + raise CircuitError(f"Not sure how to combine these qubit arguments:\n {qargs}\n") def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, list]]: """Validation and handling of the arguments and its relationship. @@ -236,7 +236,7 @@ def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, elif len(qargs) >= 3: return Gate._broadcast_3_or_more_args(qargs) else: - raise CircuitError("This gate cannot handle %i arguments" % len(qargs)) + raise CircuitError(f"This gate cannot handle {len(qargs)} arguments") def validate_parameter(self, parameter): """Gate parameters should be int, float, or ParameterExpression""" diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 44155783d40..f53c5b9e9b3 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -81,7 +81,7 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt raise CircuitError("num_qubits and num_clbits must be integer.") if num_qubits < 0 or num_clbits < 0: raise CircuitError( - "bad instruction dimensions: %d qubits, %d clbits." % num_qubits, num_clbits + f"bad instruction dimensions: {num_qubits} qubits, {num_clbits} clbits." ) self._name = name self._num_qubits = num_qubits @@ -222,8 +222,9 @@ def __repr__(self) -> str: str: A representation of the Instruction instance with the name, number of qubits, classical bits and params( if any ) """ - return "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - self.name, self.num_qubits, self.num_clbits, self.params + return ( + f"Instruction(name='{self.name}', num_qubits={self.num_qubits}, " + f"num_clbits={self.num_clbits}, params={self.params})" ) def soft_compare(self, other: "Instruction") -> bool: @@ -456,7 +457,7 @@ def inverse(self, annotated: bool = False): return AnnotatedOperation(self, InverseModifier()) if self.definition is None: - raise CircuitError("inverse() not implemented for %s." % self.name) + raise CircuitError(f"inverse() not implemented for {self.name}.") from qiskit.circuit import Gate # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py b/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py index a40767154bc..bc80ef77861 100644 --- a/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py @@ -153,7 +153,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py index a27c57ef28f..cc34d3631f5 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py +++ b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py @@ -122,7 +122,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py index 509433af557..3d84e64ccb1 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py @@ -202,7 +202,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) if len(self.breakpoints) != len(self.slopes) or len(self.breakpoints) != len(self.offsets): diff --git a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py index 7e79ed04da1..741b920e368 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py @@ -237,7 +237,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) if len(self.breakpoints) != len(self.coeffs) + 1: diff --git a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py index 13fb8229881..4f04a04dd52 100644 --- a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py @@ -248,7 +248,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 43e80ead883..1d9ad7f7b08 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -154,8 +154,8 @@ def _define_from_int(self): # Raise if number of bits is greater than num_qubits if len(intstr) > self.num_qubits: raise QiskitError( - "StatePreparation integer has %s bits, but this exceeds the" - " number of qubits in the circuit, %s." % (len(intstr), self.num_qubits) + f"StatePreparation integer has {len(intstr)} bits, but this exceeds the" + f" number of qubits in the circuit, {self.num_qubits}." ) for qubit, bit in enumerate(intstr): @@ -212,9 +212,9 @@ def broadcast_arguments(self, qargs, cargs): if self.num_qubits != len(flat_qargs): raise QiskitError( - "StatePreparation parameter vector has %d elements, therefore expects %s " - "qubits. However, %s were provided." - % (2**self.num_qubits, self.num_qubits, len(flat_qargs)) + f"StatePreparation parameter vector has {2**self.num_qubits}" + f" elements, therefore expects {self.num_qubits} " + f"qubits. However, {len(flat_qargs)} were provided." ) yield flat_qargs, [] @@ -226,8 +226,8 @@ def validate_parameter(self, parameter): if parameter in ["0", "1", "+", "-", "l", "r"]: return parameter raise CircuitError( - "invalid param label {} for instruction {}. Label should be " - "0, 1, +, -, l, or r ".format(type(parameter), self.name) + f"invalid param label {type(parameter)} for instruction {self.name}. Label should be " + "0, 1, +, -, l, or r " ) # StatePreparation instruction parameter can be int, float, and complex. diff --git a/qiskit/circuit/library/graph_state.py b/qiskit/circuit/library/graph_state.py index ceefff7971d..89d1edb035f 100644 --- a/qiskit/circuit/library/graph_state.py +++ b/qiskit/circuit/library/graph_state.py @@ -74,7 +74,7 @@ def __init__(self, adjacency_matrix: list | np.ndarray) -> None: raise CircuitError("The adjacency matrix must be symmetric.") num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name="graph: %s" % (adjacency_matrix)) + circuit = QuantumCircuit(num_qubits, name=f"graph: {adjacency_matrix}") circuit.h(range(num_qubits)) for i in range(num_qubits): diff --git a/qiskit/circuit/library/hamiltonian_gate.py b/qiskit/circuit/library/hamiltonian_gate.py index 2997d01ed48..d920d787387 100644 --- a/qiskit/circuit/library/hamiltonian_gate.py +++ b/qiskit/circuit/library/hamiltonian_gate.py @@ -103,8 +103,7 @@ def __array__(self, dtype=None, copy=None): time = float(self.params[1]) except TypeError as ex: raise TypeError( - "Unable to generate Unitary matrix for " - "unbound t parameter {}".format(self.params[1]) + f"Unable to generate Unitary matrix for unbound t parameter {self.params[1]}" ) from ex arr = scipy.linalg.expm(-1j * self.params[0] * time) dtype = complex if dtype is None else dtype diff --git a/qiskit/circuit/library/hidden_linear_function.py b/qiskit/circuit/library/hidden_linear_function.py index 1140f1866f0..b68fda7f8fc 100644 --- a/qiskit/circuit/library/hidden_linear_function.py +++ b/qiskit/circuit/library/hidden_linear_function.py @@ -82,7 +82,7 @@ def __init__(self, adjacency_matrix: Union[List[List[int]], np.ndarray]) -> None raise CircuitError("The adjacency matrix must be symmetric.") num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name="hlf: %s" % adjacency_matrix) + circuit = QuantumCircuit(num_qubits, name=f"hlf: {adjacency_matrix}") circuit.h(range(num_qubits)) for i in range(num_qubits): diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 430edfd94f3..25f6c27bbe1 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -441,9 +441,8 @@ def ordered_parameters(self, parameters: ParameterVector | list[Parameter]) -> N ): raise ValueError( "The length of ordered parameters must be equal to the number of " - "settable parameters in the circuit ({}), but is {}".format( - self.num_parameters_settable, len(parameters) - ) + f"settable parameters in the circuit ({self.num_parameters_settable})," + f" but is {len(parameters)}" ) self._ordered_parameters = parameters self._invalidate() diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index d62e12c4d94..43869c0c54c 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -97,20 +97,18 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: valid = False if raise_on_failure: raise ValueError( - "The number of qubits of the initial state {} does not match " - "the number of qubits of the cost operator {}".format( - self.initial_state.num_qubits, self.num_qubits - ) + f"The number of qubits of the initial state {self.initial_state.num_qubits}" + " does not match " + f"the number of qubits of the cost operator {self.num_qubits}" ) if self.mixer_operator is not None and self.mixer_operator.num_qubits != self.num_qubits: valid = False if raise_on_failure: raise ValueError( - "The number of qubits of the mixer {} does not match " - "the number of qubits of the cost operator {}".format( - self.mixer_operator.num_qubits, self.num_qubits - ) + f"The number of qubits of the mixer {self.mixer_operator.num_qubits}" + f" does not match " + f"the number of qubits of the cost operator {self.num_qubits}" ) return valid diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index 2db6a80eedc..f6ae5fd6ebd 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -112,8 +112,6 @@ def _check_unitary(circuit): for instruction in circuit.data: if not isinstance(instruction.operation, (Gate, Barrier)): raise CircuitError( - ( - "One or more instructions cannot be converted to" - ' a gate. "{}" is not a gate instruction' - ).format(instruction.operation.name) + "One or more instructions cannot be converted to" + f' a gate. "{instruction.operation.name}" is not a gate instruction' ) diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 62c1e33b962..0eef2518a85 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -344,7 +344,7 @@ def _generate_gray_code(num_bits): result = [0] for i in range(num_bits): result += [x + 2**i for x in reversed(result)] - return [format(x, "0%sb" % num_bits) for x in result] + return [format(x, f"0{num_bits}b") for x in result] def _gray_code_chain(q, num_ctrl_qubits, gate): diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index abe4e61adf6..825679f7d4f 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -109,8 +109,8 @@ def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False): if allow_unknown_parameters: return self raise CircuitError( - "Cannot bind Parameters ({}) not present in " - "expression.".format([str(p) for p in parameter_map]) + f"Cannot bind Parameters ({[str(p) for p in parameter_map]}) not present in " + "expression." ) @property diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index f881e09333d..7f839677b90 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -140,7 +140,7 @@ def bind( raise ZeroDivisionError( "Binding provided for expression " "results in division by zero " - "(Expression: {}, Bindings: {}).".format(self, parameter_values) + f"(Expression: {self}, Bindings: {parameter_values})." ) return ParameterExpression(free_parameter_symbols, bound_symbol_expr) @@ -199,8 +199,8 @@ def _raise_if_passed_unknown_parameters(self, parameters): unknown_parameters = parameters - self.parameters if unknown_parameters: raise CircuitError( - "Cannot bind Parameters ({}) not present in " - "expression.".format([str(p) for p in unknown_parameters]) + f"Cannot bind Parameters ({[str(p) for p in unknown_parameters]}) not present in " + "expression." ) def _raise_if_passed_nan(self, parameter_values): @@ -404,8 +404,8 @@ def __complex__(self): except (TypeError, RuntimeError) as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a complex.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to a complex." ) from None raise TypeError("could not cast expression to complex") from exc @@ -416,8 +416,8 @@ def __float__(self): except (TypeError, RuntimeError) as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a float.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to a float." ) from None # In symengine, if an expression was complex at any time, its type is likely to have # stayed "complex" even when the imaginary part symbolically (i.e. exactly) @@ -436,8 +436,8 @@ def __int__(self): except RuntimeError as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to an int.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to an int." ) from None raise TypeError("could not cast expression to int") from exc diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index e3fbf40da68..010a91e3639 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1067,8 +1067,9 @@ def __init__( if not valid_reg_size: raise CircuitError( - "Circuit args must be Registers or integers. (%s '%s' was " - "provided)" % ([type(reg).__name__ for reg in regs], regs) + "Circuit args must be Registers or integers. (" + f"{[type(reg).__name__ for reg in regs]} '{regs}' was " + "provided)" ) regs = tuple(int(reg) for reg in regs) # cast to int @@ -1659,7 +1660,7 @@ def power( raise CircuitError( "Cannot raise a parameterized circuit to a non-positive power " "or matrix-power, please bind the free parameters: " - "{}".format(self.parameters) + f"{self.parameters}" ) try: @@ -2957,14 +2958,14 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: raise CircuitError( "QuantumCircuit parameters can be Registers or Integers." " If Integers, up to 2 arguments. QuantumCircuit was called" - " with %s." % (regs,) + f" with {(regs,)}." ) for register in regs: if isinstance(register, Register) and any( register.name == reg.name for reg in self.qregs + self.cregs ): - raise CircuitError('register name "%s" already exists' % register.name) + raise CircuitError(f'register name "{register.name}" already exists') if isinstance(register, AncillaRegister): for bit in register: @@ -3020,7 +3021,7 @@ def add_bits(self, bits: Iterable[Bit]) -> None: else: raise CircuitError( "Expected an instance of Qubit, Clbit, or " - "AncillaQubit, but was passed {}".format(bit) + f"AncillaQubit, but was passed {bit}" ) def find_bit(self, bit: Bit) -> BitLocations: diff --git a/qiskit/circuit/quantumregister.py b/qiskit/circuit/quantumregister.py index 2ae815b1d17..97d1392698e 100644 --- a/qiskit/circuit/quantumregister.py +++ b/qiskit/circuit/quantumregister.py @@ -43,7 +43,7 @@ def __init__(self, register=None, index=None): super().__init__(register, index) else: raise CircuitError( - "Qubit needs a QuantumRegister and %s was provided" % type(register).__name__ + f"Qubit needs a QuantumRegister and {type(register).__name__} was provided" ) diff --git a/qiskit/circuit/register.py b/qiskit/circuit/register.py index e927d10e736..39345705aae 100644 --- a/qiskit/circuit/register.py +++ b/qiskit/circuit/register.py @@ -67,7 +67,7 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): if (size, bits) == (None, None) or (size is not None and bits is not None): raise CircuitError( "Exactly one of the size or bits arguments can be " - "provided. Provided size=%s bits=%s." % (size, bits) + f"provided. Provided size={size} bits={bits}." ) # validate (or cast) size @@ -81,20 +81,18 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): if not valid_size: raise CircuitError( - "Register size must be an integer. (%s '%s' was provided)" - % (type(size).__name__, size) + f"Register size must be an integer. ({type(size).__name__} '{size}' was provided)" ) size = int(size) # cast to int if size < 0: raise CircuitError( - "Register size must be non-negative (%s '%s' was provided)" - % (type(size).__name__, size) + f"Register size must be non-negative ({type(size).__name__} '{size}' was provided)" ) # validate (or cast) name if name is None: - name = "%s%i" % (self.prefix, next(self.instances_counter)) + name = f"{self.prefix}{next(self.instances_counter)}" else: try: name = str(name) @@ -108,7 +106,7 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): self._size = size self._hash = hash((type(self), self._name, self._size)) - self._repr = "%s(%d, '%s')" % (self.__class__.__qualname__, self.size, self.name) + self._repr = f"{self.__class__.__qualname__}({self.size}, '{self.name}')" if bits is not None: # check duplicated bits if self._size != len(set(bits)): diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py index d3614b74782..334b9683ae9 100644 --- a/qiskit/circuit/tools/pi_check.py +++ b/qiskit/circuit/tools/pi_check.py @@ -104,9 +104,9 @@ def normalize(single_inpt): if power[0].shape[0]: if output == "qasm": if ndigits is None: - str_out = "{}".format(single_inpt) + str_out = str(single_inpt) else: - str_out = "{:.{}g}".format(single_inpt, ndigits) + str_out = f"{single_inpt:.{ndigits}g}" elif output == "latex": str_out = f"{neg_str}{pi}^{power[0][0] + 2}" elif output == "mpl": @@ -119,9 +119,9 @@ def normalize(single_inpt): # multiple or power of pi, since no fractions will exceed MAX_FRAC * pi if abs(single_inpt) >= (MAX_FRAC * np.pi): if ndigits is None: - str_out = "{}".format(single_inpt) + str_out = str(single_inpt) else: - str_out = "{:.{}g}".format(single_inpt, ndigits) + str_out = f"{single_inpt:.{ndigits}g}" return str_out # Fourth check is for fractions for 1*pi in the numer and any diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index a6c5212e233..522e1c503dd 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -34,7 +34,7 @@ def _log_assembly_time(start_time, end_time): - log_msg = "Total Assembly Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Assembly Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) @@ -311,8 +311,8 @@ def _parse_common_args( raise QiskitError("Argument 'shots' should be of type 'int'") elif max_shots and max_shots < shots: raise QiskitError( - "Number of shots specified: %s exceeds max_shots property of the " - "backend: %s." % (shots, max_shots) + f"Number of shots specified: {max_shots} exceeds max_shots property of the " + f"backend: {max_shots}." ) dynamic_reprate_enabled = getattr(backend_config, "dynamic_reprate_enabled", False) @@ -397,9 +397,8 @@ def _check_lo_freqs( raise QiskitError(f"Each element of {lo_type} LO range must be a 2d list.") if freq < freq_range[0] or freq > freq_range[1]: raise QiskitError( - "Qubit {} {} LO frequency is {}. The range is [{}, {}].".format( - i, lo_type, freq, freq_range[0], freq_range[1] - ) + f"Qubit {i} {lo_type} LO frequency is {freq}. " + f"The range is [{freq_range[0]}, {freq_range[1]}]." ) @@ -429,9 +428,8 @@ def _parse_pulse_args( if meas_level not in getattr(backend_config, "meas_levels", [MeasLevel.CLASSIFIED]): raise QiskitError( - ("meas_level = {} not supported for backend {}, only {} is supported").format( - meas_level, backend_config.backend_name, backend_config.meas_levels - ) + f"meas_level = {meas_level} not supported for backend " + f"{backend_config.backend_name}, only {backend_config.meas_levels} is supported" ) meas_map = meas_map or getattr(backend_config, "meas_map", None) @@ -522,14 +520,12 @@ def _parse_rep_delay( if rep_delay_range is not None and isinstance(rep_delay_range, list): if len(rep_delay_range) != 2: raise QiskitError( - "Backend rep_delay_range {} must be a list with two entries.".format( - rep_delay_range - ) + f"Backend rep_delay_range {rep_delay_range} must be a list with two entries." ) if not rep_delay_range[0] <= rep_delay <= rep_delay_range[1]: raise QiskitError( - "Supplied rep delay {} not in the supported " - "backend range {}".format(rep_delay, rep_delay_range) + f"Supplied rep delay {rep_delay} not in the supported " + f"backend range {rep_delay_range}" ) rep_delay = rep_delay * 1e6 # convert sec to μs diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py index 0a30b07a49b..f141902b706 100644 --- a/qiskit/compiler/scheduler.py +++ b/qiskit/compiler/scheduler.py @@ -31,7 +31,7 @@ def _log_schedule_time(start_time, end_time): - log_msg = "Total Scheduling Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Scheduling Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 9dd316839a0..183e260739b 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -405,7 +405,7 @@ def _check_circuits_coupling_map(circuits, cmap, backend): def _log_transpile_time(start_time, end_time): - log_msg = "Total Transpile Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Transpile Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) @@ -476,7 +476,7 @@ def _parse_output_name(output_name, circuits): else: raise TranspilerError( "The parameter output_name should be a string or a" - "list of strings: %s was used." % type(output_name) + f"list of strings: {type(output_name)} was used." ) else: return [circuit.name for circuit in circuits] diff --git a/qiskit/converters/circuit_to_gate.py b/qiskit/converters/circuit_to_gate.py index 39eed1053eb..c9f9ac6e1af 100644 --- a/qiskit/converters/circuit_to_gate.py +++ b/qiskit/converters/circuit_to_gate.py @@ -64,10 +64,8 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label for instruction in circuit.data: if not _check_is_gate(instruction.operation): raise QiskitError( - ( - "One or more instructions cannot be converted to" - ' a gate. "{}" is not a gate instruction' - ).format(instruction.operation.name) + "One or more instructions cannot be converted to" + f' a gate. "{instruction.operation.name}" is not a gate instruction' ) if parameter_map is None: @@ -77,10 +75,8 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label if parameter_dict.keys() != circuit.parameters: raise QiskitError( - ( - "parameter_map should map all circuit parameters. " - "Circuit parameters: {}, parameter_map: {}" - ).format(circuit.parameters, parameter_dict) + "parameter_map should map all circuit parameters. " + f"Circuit parameters: {circuit.parameters}, parameter_map: {parameter_dict}" ) gate = Gate( diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 1a5907c3ec8..571e330eb0d 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -89,10 +89,8 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None if parameter_dict.keys() != circuit.parameters: raise QiskitError( - ( - "parameter_map should map all circuit parameters. " - "Circuit parameters: {}, parameter_map: {}" - ).format(circuit.parameters, parameter_dict) + "parameter_map should map all circuit parameters. " + f"Circuit parameters: {circuit.parameters}, parameter_map: {parameter_dict}" ) out_instruction = Instruction( diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 96562a37b15..686951f26fc 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -271,7 +271,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGCircuitError("duplicate qubits %s" % duplicate_qubits) + raise DAGCircuitError(f"duplicate qubits {duplicate_qubits}") for qubit in qubits: self.qubits.append(qubit) @@ -285,7 +285,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGCircuitError("duplicate clbits %s" % duplicate_clbits) + raise DAGCircuitError(f"duplicate clbits {duplicate_clbits}") for clbit in clbits: self.clbits.append(clbit) @@ -297,7 +297,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGCircuitError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGCircuitError("duplicate register %s" % qreg.name) + raise DAGCircuitError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -315,7 +315,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGCircuitError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGCircuitError("duplicate register %s" % creg.name) + raise DAGCircuitError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): @@ -451,17 +451,17 @@ def remove_clbits(self, *clbits): """ if any(not isinstance(clbit, Clbit) for clbit in clbits): raise DAGCircuitError( - "clbits not of type Clbit: %s" % [b for b in clbits if not isinstance(b, Clbit)] + f"clbits not of type Clbit: {[b for b in clbits if not isinstance(b, Clbit)]}" ) clbits = set(clbits) unknown_clbits = clbits.difference(self.clbits) if unknown_clbits: - raise DAGCircuitError("clbits not in circuit: %s" % unknown_clbits) + raise DAGCircuitError(f"clbits not in circuit: {unknown_clbits}") busy_clbits = {bit for bit in clbits if not self._is_wire_idle(bit)} if busy_clbits: - raise DAGCircuitError("clbits not idle: %s" % busy_clbits) + raise DAGCircuitError(f"clbits not idle: {busy_clbits}") # remove any references to bits cregs_to_remove = {creg for creg in self.cregs.values() if not clbits.isdisjoint(creg)} @@ -487,13 +487,13 @@ def remove_cregs(self, *cregs): """ if any(not isinstance(creg, ClassicalRegister) for creg in cregs): raise DAGCircuitError( - "cregs not of type ClassicalRegister: %s" - % [r for r in cregs if not isinstance(r, ClassicalRegister)] + "cregs not of type ClassicalRegister: " + f"{[r for r in cregs if not isinstance(r, ClassicalRegister)]}" ) unknown_cregs = set(cregs).difference(self.cregs.values()) if unknown_cregs: - raise DAGCircuitError("cregs not in circuit: %s" % unknown_cregs) + raise DAGCircuitError(f"cregs not in circuit: {unknown_cregs}") for creg in cregs: del self.cregs[creg.name] @@ -517,17 +517,17 @@ def remove_qubits(self, *qubits): """ if any(not isinstance(qubit, Qubit) for qubit in qubits): raise DAGCircuitError( - "qubits not of type Qubit: %s" % [b for b in qubits if not isinstance(b, Qubit)] + f"qubits not of type Qubit: {[b for b in qubits if not isinstance(b, Qubit)]}" ) qubits = set(qubits) unknown_qubits = qubits.difference(self.qubits) if unknown_qubits: - raise DAGCircuitError("qubits not in circuit: %s" % unknown_qubits) + raise DAGCircuitError(f"qubits not in circuit: {unknown_qubits}") busy_qubits = {bit for bit in qubits if not self._is_wire_idle(bit)} if busy_qubits: - raise DAGCircuitError("qubits not idle: %s" % busy_qubits) + raise DAGCircuitError(f"qubits not idle: {busy_qubits}") # remove any references to bits qregs_to_remove = {qreg for qreg in self.qregs.values() if not qubits.isdisjoint(qreg)} @@ -553,13 +553,13 @@ def remove_qregs(self, *qregs): """ if any(not isinstance(qreg, QuantumRegister) for qreg in qregs): raise DAGCircuitError( - "qregs not of type QuantumRegister: %s" - % [r for r in qregs if not isinstance(r, QuantumRegister)] + f"qregs not of type QuantumRegister: " + f"{[r for r in qregs if not isinstance(r, QuantumRegister)]}" ) unknown_qregs = set(qregs).difference(self.qregs.values()) if unknown_qregs: - raise DAGCircuitError("qregs not in circuit: %s" % unknown_qregs) + raise DAGCircuitError(f"qregs not in circuit: {unknown_qregs}") for qreg in qregs: del self.qregs[qreg.name] @@ -581,13 +581,13 @@ def _is_wire_idle(self, wire): DAGCircuitError: the wire is not in the circuit. """ if wire not in self._wires: - raise DAGCircuitError("wire %s not in circuit" % wire) + raise DAGCircuitError(f"wire {wire} not in circuit") try: child = next(self.successors(self.input_map[wire])) except StopIteration as e: raise DAGCircuitError( - "Invalid dagcircuit input node %s has no output" % self.input_map[wire] + f"Invalid dagcircuit input node {self.input_map[wire]} has no output" ) from e return child is self.output_map[wire] @@ -950,12 +950,11 @@ def _reject_new_register(reg): # the mapped wire should already exist if m_wire not in dag.output_map: raise DAGCircuitError( - "wire %s[%d] not in self" % (m_wire.register.name, m_wire.index) + f"wire {m_wire.register.name}[{m_wire.index}] not in self" ) if nd.wire not in other._wires: raise DAGCircuitError( - "inconsistent wire type for %s[%d] in other" - % (nd.register.name, nd.wire.index) + f"inconsistent wire type for {nd.register.name}[{nd.wire.index}] in other" ) # If it's a Var wire, we already checked that it exists in the destination. elif isinstance(nd, DAGOutNode): @@ -974,7 +973,7 @@ def _reject_new_register(reg): op.target = variable_mapper.map_target(op.target) dag.apply_operation_back(op, m_qargs, m_cargs, check=False) else: - raise DAGCircuitError("bad node type %s" % type(nd)) + raise DAGCircuitError(f"bad node type {type(nd)}") if not inplace: return dag @@ -1632,10 +1631,10 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ if node.op.num_qubits != op.num_qubits or node.op.num_clbits != op.num_clbits: raise DAGCircuitError( - "Cannot replace node of width ({} qubits, {} clbits) with " - "operation of mismatched width ({} qubits, {} clbits).".format( - node.op.num_qubits, node.op.num_clbits, op.num_qubits, op.num_clbits - ) + f"Cannot replace node of width ({node.op.num_qubits} qubits, " + f"{node.op.num_clbits} clbits) with " + f"operation of mismatched width ({op.num_qubits} qubits, " + f"{op.num_clbits} clbits)." ) # This might include wires that are inherent to the node, like in its `condition` or @@ -1953,8 +1952,8 @@ def remove_op_node(self, node): """ if not isinstance(node, DAGOpNode): raise DAGCircuitError( - 'The method remove_op_node only works on DAGOpNodes. A "%s" ' - "node type was wrongly provided." % type(node) + f'The method remove_op_node only works on DAGOpNodes. A "{type(node)}" ' + "node type was wrongly provided." ) self._multi_graph.remove_node_retain_edges( @@ -2182,7 +2181,7 @@ def nodes_on_wire(self, wire, only_ops=False): current_node = self.input_map.get(wire, None) if not current_node: - raise DAGCircuitError("The given wire %s is not present in the circuit" % str(wire)) + raise DAGCircuitError(f"The given wire {str(wire)} is not present in the circuit") more_nodes = True while more_nodes: diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index 4316c947140..63b91f92063 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -187,7 +187,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGDependencyError("duplicate qubits %s" % duplicate_qubits) + raise DAGDependencyError(f"duplicate qubits {duplicate_qubits}") self.qubits.extend(qubits) @@ -198,7 +198,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGDependencyError("duplicate clbits %s" % duplicate_clbits) + raise DAGDependencyError(f"duplicate clbits {duplicate_clbits}") self.clbits.extend(clbits) @@ -207,7 +207,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGDependencyError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGDependencyError("duplicate register %s" % qreg.name) + raise DAGDependencyError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -219,7 +219,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGDependencyError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGDependencyError("duplicate register %s" % creg.name) + raise DAGDependencyError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): diff --git a/qiskit/dagcircuit/dagdependency_v2.py b/qiskit/dagcircuit/dagdependency_v2.py index cb5d447162c..e50c47b24f9 100644 --- a/qiskit/dagcircuit/dagdependency_v2.py +++ b/qiskit/dagcircuit/dagdependency_v2.py @@ -247,7 +247,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGDependencyError("duplicate qubits %s" % duplicate_qubits) + raise DAGDependencyError(f"duplicate qubits {duplicate_qubits}") for qubit in qubits: self.qubits.append(qubit) @@ -260,7 +260,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGDependencyError("duplicate clbits %s" % duplicate_clbits) + raise DAGDependencyError(f"duplicate clbits {duplicate_clbits}") for clbit in clbits: self.clbits.append(clbit) @@ -271,7 +271,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGDependencyError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGDependencyError("duplicate register %s" % qreg.name) + raise DAGDependencyError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -288,7 +288,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGDependencyError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGDependencyError("duplicate register %s" % creg.name) + raise DAGDependencyError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): diff --git a/qiskit/dagcircuit/dagdepnode.py b/qiskit/dagcircuit/dagdepnode.py index fe63f57d3d4..cc00db9725c 100644 --- a/qiskit/dagcircuit/dagdepnode.py +++ b/qiskit/dagcircuit/dagdepnode.py @@ -83,7 +83,7 @@ def __init__( def op(self): """Returns the Instruction object corresponding to the op for the node, else None""" if not self.type or self.type != "op": - raise QiskitError("The node %s is not an op node" % (str(self))) + raise QiskitError(f"The node {str(self)} is not an op node") return self._op @op.setter diff --git a/qiskit/passmanager/flow_controllers.py b/qiskit/passmanager/flow_controllers.py index c7d952d048d..dcfcba70458 100644 --- a/qiskit/passmanager/flow_controllers.py +++ b/qiskit/passmanager/flow_controllers.py @@ -84,7 +84,7 @@ def iter_tasks(self, state: PassManagerState) -> Generator[Task, PassManagerStat return # Remove stored tasks from the completed task collection for next loop state.workflow_status.completed_passes.difference_update(self.tasks) - raise PassManagerError("Maximum iteration reached. max_iteration=%i" % max_iteration) + raise PassManagerError(f"Maximum iteration reached. max_iteration={max_iteration}") class ConditionalController(BaseController): diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py index 8d3a4e9aa69..99527bf584e 100644 --- a/qiskit/passmanager/passmanager.py +++ b/qiskit/passmanager/passmanager.py @@ -130,7 +130,7 @@ def __add__(self, other): return new_passmanager except PassManagerError as ex: raise TypeError( - "unsupported operand type + for %s and %s" % (self.__class__, other.__class__) + f"unsupported operand type + for {self.__class__} and {other.__class__}" ) from ex @abstractmethod diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 2e551cc311e..ed9fd3fdbb8 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -95,7 +95,7 @@ def __init__(self, configuration, provider=None, **fields): if fields: for field in fields: if field not in self._options.data: - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_config(**fields) @classmethod @@ -129,7 +129,7 @@ def set_options(self, **fields): """ for field in fields: if not hasattr(self._options, field): - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_options(**fields) def configuration(self): @@ -352,7 +352,7 @@ def __init__( if fields: for field in fields: if field not in self._options.data: - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_config(**fields) self.name = name """Name of the backend.""" @@ -598,7 +598,7 @@ def set_options(self, **fields): """ for field in fields: if not hasattr(self._options, field): - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_options(**fields) @property diff --git a/qiskit/providers/basic_provider/basic_provider_tools.py b/qiskit/providers/basic_provider/basic_provider_tools.py index b2670cc0977..786815dda53 100644 --- a/qiskit/providers/basic_provider/basic_provider_tools.py +++ b/qiskit/providers/basic_provider/basic_provider_tools.py @@ -66,7 +66,7 @@ def single_gate_matrix(gate: str, params: list[float] | None = None) -> np.ndarr if gate in SINGLE_QUBIT_GATES: gc = SINGLE_QUBIT_GATES[gate] else: - raise QiskitError("Gate is not a valid basis gate for this simulator: %s" % gate) + raise QiskitError(f"Gate is not a valid basis gate for this simulator: {gate}") return gc(*params).to_matrix() diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index 978e1dad56f..9971bf36725 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -208,7 +208,7 @@ def _build_basic_target(self) -> Target: target.add_instruction(UnitaryGate, name="unitary") else: raise BasicProviderError( - "Gate is not a valid basis gate for this simulator: %s" % name + f"Gate is not a valid basis gate for this simulator: {name}" ) return target @@ -531,7 +531,7 @@ def run( for key, value in backend_options.items(): if not hasattr(self.options, key): warnings.warn( - "Option %s is not used by this backend" % key, UserWarning, stacklevel=2 + f"Option {key} is not used by this backend", UserWarning, stacklevel=2 ) else: out_options[key] = value diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index d84aba46371..4a638f31557 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -143,8 +143,8 @@ def run(self, run_input, **kwargs): pulse_job = False if pulse_job is None: raise QiskitError( - "Invalid input object %s, must be either a " - "QuantumCircuit, Schedule, or a list of either" % circuits + f"Invalid input object {circuits}, must be either a " + "QuantumCircuit, Schedule, or a list of either" ) if pulse_job: raise QiskitError("Pulse simulation is currently not supported for fake backends.") diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index e806c75ea3a..1ac0484d775 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -496,8 +496,8 @@ def run(self, run_input, **options): pulse_job = False if pulse_job is None: # submitted job is invalid raise QiskitError( - "Invalid input object %s, must be either a " - "QuantumCircuit, Schedule, or a list of either" % circuits + f"Invalid input object {circuits}, must be either a " + "QuantumCircuit, Schedule, or a list of either" ) if pulse_job: # pulse job raise QiskitError("Pulse simulation is currently not supported for V2 backends.") diff --git a/qiskit/providers/models/backendproperties.py b/qiskit/providers/models/backendproperties.py index 3b5b9c5e010..332aac7c5ed 100644 --- a/qiskit/providers/models/backendproperties.py +++ b/qiskit/providers/models/backendproperties.py @@ -404,9 +404,9 @@ def qubit_property( if name is not None: result = result[name] except KeyError as ex: + formatted_name = "y '" + name + "'" if name else "ies" raise BackendPropertyError( - "Couldn't find the propert{name} for qubit " - "{qubit}.".format(name="y '" + name + "'" if name else "ies", qubit=qubit) + f"Couldn't find the propert{formatted_name} for qubit {qubit}." ) from ex return result diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index 13becb1c956..7c1864bad9e 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -296,9 +296,4 @@ def __str__(self): meas_freqs = [freq / 1e9 for freq in self.meas_freq_est] qfreq = f"Qubit Frequencies [GHz]\n{qubit_freqs}" mfreq = f"Measurement Frequencies [GHz]\n{meas_freqs} " - return "<{name}({insts}{qfreq}\n{mfreq})>".format( - name=self.__class__.__name__, - insts=str(self.instruction_schedule_map), - qfreq=qfreq, - mfreq=mfreq, - ) + return f"<{self.__class__.__name__}({str(self.instruction_schedule_map)}{qfreq}\n{mfreq})>" diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index 8b2bffc52d8..fe4e7303a67 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -170,7 +170,7 @@ def __init__(self, **kwargs): def __repr__(self): items = (f"{k}={v!r}" for k, v in self._fields.items()) - return "{}({})".format(type(self).__name__, ", ".join(items)) + return f"{type(self).__name__}({', '.join(items)})" def __eq__(self, other): if isinstance(self, Options) and isinstance(other, Options): @@ -211,7 +211,7 @@ def set_validator(self, field, validator_value): """ if field not in self._fields: - raise KeyError("Field '%s' is not present in this options object" % field) + raise KeyError(f"Field '{field}' is not present in this options object") if isinstance(validator_value, tuple): if len(validator_value) != 2: raise ValueError( diff --git a/qiskit/pulse/configuration.py b/qiskit/pulse/configuration.py index 4668152973f..1bfd1f13e2e 100644 --- a/qiskit/pulse/configuration.py +++ b/qiskit/pulse/configuration.py @@ -55,11 +55,9 @@ def __init__(self, name: str | None = None, **params): self.params = params def __repr__(self): - return "{}({}{})".format( - self.__class__.__name__, - "'" + self.name + "', " or "", - ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()), - ) + name_repr = "'" + self.name + "', " + params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) + return f"{self.__class__.__name__}({name_repr}{params_repr})" def __eq__(self, other): if isinstance(other, Kernel): @@ -83,11 +81,9 @@ def __init__(self, name: str | None = None, **params): self.params = params def __repr__(self): - return "{}({}{})".format( - self.__class__.__name__, - "'" + self.name + "', " or "", - ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()), - ) + name_repr = "'" + self.name + "', " or "" + params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) + return f"{self.__class__.__name__}({name_repr}{params_repr})" def __eq__(self, other): if isinstance(other, Discriminator): @@ -184,7 +180,7 @@ def add_lo(self, channel: DriveChannel | MeasureChannel, freq: float): self.check_lo(channel, freq) self._m_lo_freq[channel] = freq else: - raise PulseError("Specified channel %s cannot be configured." % channel.name) + raise PulseError(f"Specified channel {channel.name} cannot be configured.") def add_lo_range( self, channel: DriveChannel | MeasureChannel, lo_range: LoRange | tuple[int, int] @@ -236,7 +232,7 @@ def channel_lo(self, channel: DriveChannel | MeasureChannel) -> float: if channel in self.meas_los: return self.meas_los[channel] - raise PulseError("Channel %s is not configured" % channel) + raise PulseError(f"Channel {channel} is not configured") @property def qubit_los(self) -> dict[DriveChannel, float]: diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index decda15c994..afa71b6825a 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -169,10 +169,8 @@ def assert_has( if not self.has(instruction, _to_tuple(qubits)): if instruction in self._map: raise PulseError( - "Operation '{inst}' exists, but is only defined for qubits " - "{qubits}.".format( - inst=instruction, qubits=self.qubits_with_instruction(instruction) - ) + f"Operation '{instruction}' exists, but is only defined for qubits " + f"{self.qubits_with_instruction(instruction)}." ) raise PulseError(f"Operation '{instruction}' is not defined for this system.") diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py index 066163a79b0..98fbf460c1b 100644 --- a/qiskit/pulse/instructions/acquire.py +++ b/qiskit/pulse/instructions/acquire.py @@ -138,12 +138,11 @@ def is_parameterized(self) -> bool: return isinstance(self.duration, ParameterExpression) or super().is_parameterized() def __repr__(self) -> str: - return "{}({}{}{}{}{}{})".format( - self.__class__.__name__, - self.duration, - ", " + str(self.channel), - ", " + str(self.mem_slot) if self.mem_slot else "", - ", " + str(self.reg_slot) if self.reg_slot else "", - ", " + str(self.kernel) if self.kernel else "", - ", " + str(self.discriminator) if self.discriminator else "", + mem_slot_repr = str(self.mem_slot) if self.mem_slot else "" + reg_slot_repr = str(self.reg_slot) if self.reg_slot else "" + kernel_repr = str(self.kernel) if self.kernel else "" + discriminator_repr = str(self.discriminator) if self.discriminator else "" + return ( + f"{self.__class__.__name__}({self.duration}, {str(self.channel)}, " + f"{mem_slot_repr}, {reg_slot_repr}, {kernel_repr}, {discriminator_repr})" ) diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py index ece20545b50..61ebe67777f 100644 --- a/qiskit/pulse/instructions/instruction.py +++ b/qiskit/pulse/instructions/instruction.py @@ -264,6 +264,5 @@ def __lshift__(self, time: int): def __repr__(self) -> str: operands = ", ".join(str(op) for op in self.operands) - return "{}({}{})".format( - self.__class__.__name__, operands, f", name='{self.name}'" if self.name else "" - ) + name_repr = f", name='{self.name}'" if self.name else "" + return f"{self.__class__.__name__}({operands}{name_repr})" diff --git a/qiskit/pulse/library/samplers/decorators.py b/qiskit/pulse/library/samplers/decorators.py index ac78fba8595..db6aabd7b1d 100644 --- a/qiskit/pulse/library/samplers/decorators.py +++ b/qiskit/pulse/library/samplers/decorators.py @@ -182,9 +182,9 @@ def _update_docstring(discretized_pulse: Callable, sampler_inst: Callable) -> Ca header, body = wrapped_docstring.split("\n", 1) body = textwrap.indent(body, " ") wrapped_docstring = header + body - updated_ds = """ - Discretized continuous pulse function: `{continuous_name}` using - sampler: `{sampler_name}`. + updated_ds = f""" + Discretized continuous pulse function: `{discretized_pulse.__name__}` using + sampler: `{sampler_inst.__name__}`. The first argument (time) of the continuous pulse function has been replaced with a discretized `duration` of type (int). @@ -198,12 +198,8 @@ def _update_docstring(discretized_pulse: Callable, sampler_inst: Callable) -> Ca Sampled continuous function: - {continuous_doc} - """.format( - continuous_name=discretized_pulse.__name__, - sampler_name=sampler_inst.__name__, - continuous_doc=wrapped_docstring, - ) + {wrapped_docstring} + """ discretized_pulse.__doc__ = updated_ds return discretized_pulse diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index b076bcf56cb..33d428771b2 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -570,11 +570,8 @@ def __eq__(self, other: object) -> bool: def __repr__(self) -> str: param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) - return "{}({}{})".format( - self._pulse_type, - param_repr, - f", name='{self.name}'" if self.name is not None else "", - ) + name_repr = f", name='{self.name}'" if self.name is not None else "" + return f"{self._pulse_type}({param_repr}{name_repr})" __hash__ = None diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py index e9ad9bcbc71..ad852f226ac 100644 --- a/qiskit/pulse/library/waveform.py +++ b/qiskit/pulse/library/waveform.py @@ -130,8 +130,5 @@ def __repr__(self) -> str: opt = np.get_printoptions() np.set_printoptions(threshold=50) np.set_printoptions(**opt) - return "{}({}{})".format( - self.__class__.__name__, - repr(self.samples), - f", name='{self.name}'" if self.name is not None else "", - ) + name_repr = f", name='{self.name}'" if self.name is not None else "" + return f"{self.__class__.__name__}({repr(self.samples)}{name_repr})" diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 88414cfc7e9..3a39932e5b1 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -135,10 +135,10 @@ def _measure_v1( default_sched = inst_map.get(measure_name, measure_group_qubits) except exceptions.PulseError as ex: raise exceptions.PulseError( - "We could not find a default measurement schedule called '{}'. " + f"We could not find a default measurement schedule called '{measure_name}'. " "Please provide another name using the 'measure_name' keyword " "argument. For assistance, the instructions which are defined are: " - "{}".format(measure_name, inst_map.instructions) + f"{inst_map.instructions}" ) from ex for time, inst in default_sched.instructions: if inst.channel.index not in qubits: @@ -203,10 +203,10 @@ def _measure_v2( schedule += _schedule_remapping_memory_slot(default_sched, qubit_mem_slots) except KeyError as ex: raise exceptions.PulseError( - "We could not find a default measurement schedule called '{}'. " + f"We could not find a default measurement schedule called '{measure_name}'. " "Please provide another name using the 'measure_name' keyword " "argument. For assistance, the instructions which are defined are: " - "{}".format(measure_name, target.instructions) + f"{target.instructions}" ) from ex return schedule diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py index 8e31faebf77..e9cd4917a7c 100644 --- a/qiskit/pulse/parser.py +++ b/qiskit/pulse/parser.py @@ -124,13 +124,11 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio self._locals_dict[key] = val else: raise PulseError( - "%s got multiple values for argument '%s'" - % (self.__class__.__name__, key) + f"{self.__class__.__name__} got multiple values for argument '{key}'" ) else: raise PulseError( - "%s got an unexpected keyword argument '%s'" - % (self.__class__.__name__, key) + f"{self.__class__.__name__} got an unexpected keyword argument '{key}'" ) expr = self.visit(self._tree) @@ -139,7 +137,7 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio if self._partial_binding: return PulseExpression(expr, self._partial_binding) else: - raise PulseError("Parameters %s are not all bound." % self.params) + raise PulseError(f"Parameters {self.params} are not all bound.") return expr.body.value @staticmethod @@ -160,7 +158,7 @@ def _match_ops(opr: ast.AST, opr_dict: dict, *args) -> complex: for op_type, op_func in opr_dict.items(): if isinstance(opr, op_type): return op_func(*args) - raise PulseError("Operator %s is not supported." % opr.__class__.__name__) + raise PulseError(f"Operator {opr.__class__.__name__} is not supported.") def visit_Expression(self, node: ast.Expression) -> ast.Expression: """Evaluate children nodes of expression. @@ -273,7 +271,7 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: node.args = [self.visit(arg) for arg in node.args] if all(isinstance(arg, ast.Constant) for arg in node.args): if node.func.id not in self._math_ops: - raise PulseError("Function %s is not supported." % node.func.id) + raise PulseError(f"Function {node.func.id} is not supported.") _args = [arg.value for arg in node.args] _val = self._math_ops[node.func.id](*_args) if not _val.imag: @@ -283,7 +281,7 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: return node def generic_visit(self, node): - raise PulseError("Unsupported node: %s" % node.__class__.__name__) + raise PulseError(f"Unsupported node: {node.__class__.__name__}") def parse_string_expr(source: str, partial_binding: bool = False) -> PulseExpression: diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index 5241da0c31d..7ccd5053e6e 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -553,17 +553,10 @@ def _add_timeslots(self, time: int, schedule: "ScheduleComponent") -> None: self._timeslots[channel].insert(index, interval) except PulseError as ex: raise PulseError( - "Schedule(name='{new}') cannot be inserted into Schedule(name='{old}') at " - "time {time} because its instruction on channel {ch} scheduled from time " - "{t0} to {tf} overlaps with an existing instruction." - "".format( - new=schedule.name or "", - old=self.name or "", - time=time, - ch=channel, - t0=interval[0], - tf=interval[1], - ) + f"Schedule(name='{schedule.name or ''}') cannot be inserted into " + f"Schedule(name='{self.name or ''}') at " + f"time {time} because its instruction on channel {channel} scheduled from time " + f"{interval[0]} to {interval[1]} overlaps with an existing instruction." ) from ex _check_nonnegative_timeslot(self._timeslots) @@ -598,10 +591,8 @@ def _remove_timeslots(self, time: int, schedule: "ScheduleComponent"): continue raise PulseError( - "Cannot find interval ({t0}, {tf}) to remove from " - "channel {ch} in Schedule(name='{name}').".format( - ch=channel, t0=interval[0], tf=interval[1], name=schedule.name - ) + f"Cannot find interval ({interval[0]}, {interval[1]}) to remove from " + f"channel {channel} in Schedule(name='{schedule.name}')." ) if not channel_timeslots: @@ -1615,8 +1606,9 @@ def __repr__(self) -> str: blocks = ", ".join([repr(instr) for instr in self.blocks[:50]]) if len(self.blocks) > 25: blocks += ", ..." - return '{}({}, name="{}", transform={})'.format( - self.__class__.__name__, blocks, name, repr(self.alignment_context) + return ( + f'{self.__class__.__name__}({blocks}, name="{name}",' + f" transform={repr(self.alignment_context)})" ) def __add__(self, other: "BlockComponent") -> "ScheduleBlock": diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py index 569219777f2..5e383972c25 100644 --- a/qiskit/pulse/transforms/alignments.py +++ b/qiskit/pulse/transforms/alignments.py @@ -398,9 +398,7 @@ def align(self, schedule: Schedule) -> Schedule: _t_center = self.duration * self.func(ind + 1) _t0 = int(_t_center - 0.5 * child.duration) if _t0 < 0 or _t0 > self.duration: - raise PulseError( - "Invalid schedule position t=%d is specified at index=%d" % (_t0, ind) - ) + raise PulseError(f"Invalid schedule position t={_t0} is specified at index={ind}") aligned.insert(_t0, child, inplace=True) return aligned diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py index ae87fbafadd..5f345917761 100644 --- a/qiskit/pulse/utils.py +++ b/qiskit/pulse/utils.py @@ -108,10 +108,9 @@ def instruction_duration_validation(duration: int): """ if isinstance(duration, ParameterExpression): raise UnassignedDurationError( - "Instruction duration {} is not assigned. " + f"Instruction duration {repr(duration)} is not assigned. " "Please bind all durations to an integer value before playing in the Schedule, " "or use ScheduleBlock to align instructions with unassigned duration." - "".format(repr(duration)) ) if not isinstance(duration, (int, np.integer)) or duration < 0: diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 9247c9233e0..46471fa087b 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -157,7 +157,7 @@ def dumps(circuit: QuantumCircuit, /) -> str: _make_unique(_escape_name(reg.name, "reg_"), register_escaped_names) ] = reg bit_labels: dict[Qubit | Clbit, str] = { - bit: "%s[%d]" % (name, idx) + bit: f"{name}[{idx}]" for name, register in register_escaped_names.items() for (idx, bit) in enumerate(register) } @@ -244,18 +244,14 @@ def _instruction_call_site(operation): else: qasm2_call = operation.name if operation.params: - qasm2_call = "{}({})".format( - qasm2_call, - ",".join([pi_check(i, output="qasm", eps=1e-12) for i in operation.params]), - ) + params = ",".join([pi_check(i, output="qasm", eps=1e-12) for i in operation.params]) + qasm2_call = f"{qasm2_call}({params})" if operation.condition is not None: if not isinstance(operation.condition[0], ClassicalRegister): raise QASM2ExportError( "OpenQASM 2 can only condition on registers, but got '{operation.condition[0]}'" ) - qasm2_call = ( - "if(%s==%d) " % (operation.condition[0].name, operation.condition[1]) + qasm2_call - ) + qasm2_call = f"if({operation.condition[0].name}=={operation.condition[1]:d}) " + qasm2_call return qasm2_call diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 80c332aaab4..11374e9aca9 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -621,7 +621,7 @@ def get_channel(self, channel: str) -> channels.PulseChannel: elif prefix == channels.ControlChannel.prefix: return channels.ControlChannel(index) - raise QiskitError("Channel %s is not valid" % channel) + raise QiskitError(f"Channel {channel} is not valid") @staticmethod def disassemble_value(value_expr: Union[float, str]) -> Union[float, ParameterExpression]: @@ -827,9 +827,7 @@ def _convert_parametric_pulse( pulse_name = instruction.label except AttributeError: sorted_params = sorted(instruction.parameters.items(), key=lambda x: x[0]) - base_str = "{pulse}_{params}".format( - pulse=instruction.pulse_shape, params=str(sorted_params) - ) + base_str = f"{instruction.pulse_shape}_{str(sorted_params)}" short_pulse_id = hashlib.md5(base_str.encode("utf-8")).hexdigest()[:4] pulse_name = f"{instruction.pulse_shape}_{short_pulse_id}" params = dict(instruction.parameters) diff --git a/qiskit/qobj/pulse_qobj.py b/qiskit/qobj/pulse_qobj.py index e5f45b4d2ac..3552d83ada8 100644 --- a/qiskit/qobj/pulse_qobj.py +++ b/qiskit/qobj/pulse_qobj.py @@ -209,8 +209,8 @@ def __repr__(self): return out def __str__(self): - out = "Instruction: %s\n" % self.name - out += "\t\tt0: %s\n" % self.t0 + out = f"Instruction: {self.name}\n" + out += f"\t\tt0: {self.t0}\n" for attr in self._COMMON_ATTRS: if hasattr(self, attr): out += f"\t\t{attr}: {getattr(self, attr)}\n" @@ -434,10 +434,10 @@ def __str__(self): header = pprint.pformat(self.header.to_dict() or {}) else: header = "{}" - out += "Header:\n%s\n" % header - out += "Config:\n%s\n\n" % config + out += f"Header:\n{header}\n" + out += f"Config:\n{config}\n\n" for instruction in self.instructions: - out += "\t%s\n" % instruction + out += f"\t{instruction}\n" return out @classmethod @@ -567,23 +567,20 @@ def __init__(self, qobj_id, config, experiments, header=None): def __repr__(self): experiments_str = [repr(x) for x in self.experiments] experiments_repr = "[" + ", ".join(experiments_str) + "]" - out = "PulseQobj(qobj_id='{}', config={}, experiments={}, header={})".format( - self.qobj_id, - repr(self.config), - experiments_repr, - repr(self.header), + return ( + f"PulseQobj(qobj_id='{self.qobj_id}', config={repr(self.config)}, " + f"experiments={experiments_repr}, header={repr(self.header)})" ) - return out def __str__(self): - out = "Pulse Qobj: %s:\n" % self.qobj_id + out = f"Pulse Qobj: {self.qobj_id}:\n" config = pprint.pformat(self.config.to_dict()) - out += "Config: %s\n" % str(config) + out += f"Config: {str(config)}\n" header = pprint.pformat(self.header.to_dict()) - out += "Header: %s\n" % str(header) + out += f"Header: {str(header)}\n" out += "Experiments:\n" for experiment in self.experiments: - out += "%s" % str(experiment) + out += str(experiment) return out def to_dict(self): diff --git a/qiskit/qobj/qasm_qobj.py b/qiskit/qobj/qasm_qobj.py index 983da1dcfd3..88d775b3b77 100644 --- a/qiskit/qobj/qasm_qobj.py +++ b/qiskit/qobj/qasm_qobj.py @@ -131,7 +131,7 @@ def to_dict(self): return out_dict def __repr__(self): - out = "QasmQobjInstruction(name='%s'" % self.name + out = f"QasmQobjInstruction(name='{self.name}'" for attr in [ "params", "qubits", @@ -155,7 +155,7 @@ def __repr__(self): return out def __str__(self): - out = "Instruction: %s\n" % self.name + out = f"Instruction: {self.name}\n" for attr in [ "params", "qubits", @@ -215,21 +215,19 @@ def __init__(self, config=None, header=None, instructions=None): def __repr__(self): instructions_str = [repr(x) for x in self.instructions] instructions_repr = "[" + ", ".join(instructions_str) + "]" - out = "QasmQobjExperiment(config={}, header={}, instructions={})".format( - repr(self.config), - repr(self.header), - instructions_repr, + return ( + f"QasmQobjExperiment(config={repr(self.config)}, header={repr(self.header)}," + f" instructions={instructions_repr})" ) - return out def __str__(self): out = "\nOpenQASM2 Experiment:\n" config = pprint.pformat(self.config.to_dict()) header = pprint.pformat(self.header.to_dict()) - out += "Header:\n%s\n" % header - out += "Config:\n%s\n\n" % config + out += f"Header:\n{header}\n" + out += f"Config:\n{config}\n\n" for instruction in self.instructions: - out += "\t%s\n" % instruction + out += f"\t{instruction}\n" return out def to_dict(self): @@ -568,23 +566,20 @@ def __init__(self, qobj_id=None, config=None, experiments=None, header=None): def __repr__(self): experiments_str = [repr(x) for x in self.experiments] experiments_repr = "[" + ", ".join(experiments_str) + "]" - out = "QasmQobj(qobj_id='{}', config={}, experiments={}, header={})".format( - self.qobj_id, - repr(self.config), - experiments_repr, - repr(self.header), + return ( + f"QasmQobj(qobj_id='{self.qobj_id}', config={repr(self.config)}," + f" experiments={experiments_repr}, header={repr(self.header)})" ) - return out def __str__(self): - out = "QASM Qobj: %s:\n" % self.qobj_id + out = f"QASM Qobj: {self.qobj_id}:\n" config = pprint.pformat(self.config.to_dict()) - out += "Config: %s\n" % str(config) + out += f"Config: {str(config)}\n" header = pprint.pformat(self.header.to_dict()) - out += "Header: %s\n" % str(header) + out += f"Header: {str(header)}\n" out += "Experiments:\n" for experiment in self.experiments: - out += "%s" % str(experiment) + out += str(experiment) return out def to_dict(self): diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index db53defbcfa..0e2045d5be5 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -128,7 +128,7 @@ def _read_registers_v4(file_obj, num_registers): ) ) name = file_obj.read(data.name_size).decode("utf8") - REGISTER_ARRAY_PACK = "!%sq" % data.size + REGISTER_ARRAY_PACK = f"!{data.size}q" bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) if data.type.decode("utf8") == "q": @@ -148,7 +148,7 @@ def _read_registers(file_obj, num_registers): ) ) name = file_obj.read(data.name_size).decode("utf8") - REGISTER_ARRAY_PACK = "!%sI" % data.size + REGISTER_ARRAY_PACK = f"!{data.size}I" bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) if data.type.decode("utf8") == "q": @@ -352,7 +352,7 @@ def _read_instruction( elif gate_name == "Clifford": gate_class = Clifford else: - raise AttributeError("Invalid instruction type: %s" % gate_name) + raise AttributeError(f"Invalid instruction type: {gate_name}") if instruction.label_size <= 0: label = None @@ -507,7 +507,7 @@ def _parse_custom_operation( if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: return definition - raise ValueError("Invalid custom instruction type '%s'" % type_str) + raise ValueError(f"Invalid custom instruction type '{type_str}'") def _read_pauli_evolution_gate(file_obj, version, vectors): @@ -1031,7 +1031,7 @@ def _write_registers(file_obj, in_circ_regs, full_bits): ) ) file_obj.write(reg_name) - REGISTER_ARRAY_PACK = "!%sq" % reg.size + REGISTER_ARRAY_PACK = f"!{reg.size}q" bit_indices = [] for bit in reg: bit_indices.append(bitmap.get(bit, -1)) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index c9f0f9af479..105d4364c07 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -277,7 +277,7 @@ def _read_parameter_expression(file_obj): elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary(binary_data, _read_parameter_expression) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") symbol_map[symbol] = value return ParameterExpression(symbol_map, expr_) @@ -311,7 +311,7 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): elif symbol_key == type_keys.Value.PARAMETER_VECTOR: symbol = _read_parameter_vec(file_obj, vectors) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % symbol_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {symbol_key}") elem_key = type_keys.Value(elem_data.type) binary_data = file_obj.read(elem_data.size) @@ -331,7 +331,7 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): use_symengine=use_symengine, ) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") symbol_map[symbol] = value return ParameterExpression(symbol_map, expr_) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 34503dbab13..d89117bc6a1 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -304,10 +304,11 @@ def load( ): warnings.warn( "The qiskit version used to generate the provided QPY " - "file, %s, is newer than the current qiskit version %s. " + f"file, {'.'.join([str(x) for x in qiskit_version])}, " + f"is newer than the current qiskit version {__version__}. " "This may result in an error if the QPY file uses " "instructions not present in this current qiskit " - "version" % (".".join([str(x) for x in qiskit_version]), __version__) + "version" ) if data.qpy_version < 5: diff --git a/qiskit/quantum_info/operators/channel/quantum_channel.py b/qiskit/quantum_info/operators/channel/quantum_channel.py index 16df920e2e0..ff20feb5bf4 100644 --- a/qiskit/quantum_info/operators/channel/quantum_channel.py +++ b/qiskit/quantum_info/operators/channel/quantum_channel.py @@ -66,12 +66,9 @@ def __init__( def __repr__(self): prefix = f"{self._channel_rep}(" pad = len(prefix) * " " - return "{}{},\n{}input_dims={}, output_dims={})".format( - prefix, - np.array2string(np.asarray(self.data), separator=", ", prefix=prefix), - pad, - self.input_dims(), - self.output_dims(), + return ( + f"{prefix}{np.array2string(np.asarray(self.data), separator=', ', prefix=prefix)}" + f",\n{pad}input_dims={self.input_dims()}, output_dims={self.output_dims()})" ) def __eq__(self, other: Self): diff --git a/qiskit/quantum_info/operators/channel/superop.py b/qiskit/quantum_info/operators/channel/superop.py index 19867696ec6..f07652e22d7 100644 --- a/qiskit/quantum_info/operators/channel/superop.py +++ b/qiskit/quantum_info/operators/channel/superop.py @@ -355,8 +355,8 @@ def _append_instruction(self, obj, qargs=None): raise QiskitError(f"Cannot apply Instruction: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; " - "expected QuantumCircuit".format(obj.name, type(obj.definition)) + f"{obj.name} instruction definition is {type(obj.definition)}; " + "expected QuantumCircuit" ) qubit_indices = {bit: idx for idx, bit in enumerate(obj.definition.qubits)} for instruction in obj.definition.data: diff --git a/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py b/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py index 7104dced9df..bfe76a2f3ca 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py @@ -92,9 +92,7 @@ def _append_circuit(elem, circuit, qargs=None): raise QiskitError(f"Cannot apply Instruction: {gate.name}") if not isinstance(gate.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - gate.name, type(gate.definition) - ) + f"{gate.name} instruction definition is {type(gate.definition)}; expected QuantumCircuit" ) flat_instr = gate.definition diff --git a/qiskit/quantum_info/operators/measures.py b/qiskit/quantum_info/operators/measures.py index 617e9f64b68..293c1236ed7 100644 --- a/qiskit/quantum_info/operators/measures.py +++ b/qiskit/quantum_info/operators/measures.py @@ -93,7 +93,7 @@ def process_fidelity( if channel.dim != target.dim: raise QiskitError( "Input quantum channel and target unitary must have the same " - "dimensions ({} != {}).".format(channel.dim, target.dim) + f"dimensions ({channel.dim} != {target.dim})." ) # Validate complete-positivity and trace-preserving diff --git a/qiskit/quantum_info/operators/op_shape.py b/qiskit/quantum_info/operators/op_shape.py index 4f95126ea14..42f05a8c53a 100644 --- a/qiskit/quantum_info/operators/op_shape.py +++ b/qiskit/quantum_info/operators/op_shape.py @@ -193,7 +193,7 @@ def _validate(self, shape, raise_exception=False): if raise_exception: raise QiskitError( "Output dimensions do not match matrix shape " - "({} != {})".format(reduce(mul, self._dims_l), shape[0]) + f"({reduce(mul, self._dims_l)} != {shape[0]})" ) return False elif shape[0] != 2**self._num_qargs_l: @@ -207,7 +207,7 @@ def _validate(self, shape, raise_exception=False): if raise_exception: raise QiskitError( "Input dimensions do not match matrix shape " - "({} != {})".format(reduce(mul, self._dims_r), shape[1]) + f"({reduce(mul, self._dims_r)} != {shape[1]})" ) return False elif shape[1] != 2**self._num_qargs_r: @@ -430,7 +430,7 @@ def compose(self, other, qargs=None, front=False): if self._num_qargs_r != other._num_qargs_l or self._dims_r != other._dims_l: raise QiskitError( "Left and right compose dimensions don't match " - "({} != {})".format(self.dims_r(), other.dims_l()) + f"({self.dims_r()} != {other.dims_l()})" ) ret._dims_l = self._dims_l ret._dims_r = other._dims_r @@ -440,7 +440,7 @@ def compose(self, other, qargs=None, front=False): if self._num_qargs_l != other._num_qargs_r or self._dims_l != other._dims_r: raise QiskitError( "Left and right compose dimensions don't match " - "({} != {})".format(self.dims_l(), other.dims_r()) + f"({self.dims_l()} != {other.dims_r()})" ) ret._dims_l = other._dims_l ret._dims_r = self._dims_r @@ -453,15 +453,13 @@ def compose(self, other, qargs=None, front=False): ret._num_qargs_l = self._num_qargs_l if len(qargs) != other._num_qargs_l: raise QiskitError( - "Number of qargs does not match ({} != {})".format( - len(qargs), other._num_qargs_l - ) + f"Number of qargs does not match ({len(qargs)} != {other._num_qargs_l})" ) if self._dims_r or other._dims_r: if self.dims_r(qargs) != other.dims_l(): raise QiskitError( "Subsystem dimension do not match on specified qargs " - "{} != {}".format(self.dims_r(qargs), other.dims_l()) + f"{self.dims_r(qargs)} != {other.dims_l()}" ) dims_r = list(self.dims_r()) for i, dim in zip(qargs, other.dims_r()): @@ -475,15 +473,13 @@ def compose(self, other, qargs=None, front=False): ret._num_qargs_r = self._num_qargs_r if len(qargs) != other._num_qargs_r: raise QiskitError( - "Number of qargs does not match ({} != {})".format( - len(qargs), other._num_qargs_r - ) + f"Number of qargs does not match ({len(qargs)} != {other._num_qargs_r})" ) if self._dims_l or other._dims_l: if self.dims_l(qargs) != other.dims_r(): raise QiskitError( "Subsystem dimension do not match on specified qargs " - "{} != {}".format(self.dims_l(qargs), other.dims_r()) + f"{self.dims_l(qargs)} != {other.dims_r()}" ) dims_l = list(self.dims_l()) for i, dim in zip(qargs, other.dims_l()): @@ -508,26 +504,22 @@ def _validate_add(self, other, qargs=None): if self.dims_l(qargs) != other.dims_l(): raise QiskitError( "Cannot add shapes width different left " - "dimension on specified qargs {} != {}".format( - self.dims_l(qargs), other.dims_l() - ) + f"dimension on specified qargs {self.dims_l(qargs)} != {other.dims_l()}" ) if self.dims_r(qargs) != other.dims_r(): raise QiskitError( "Cannot add shapes width different total right " - "dimension on specified qargs{} != {}".format( - self.dims_r(qargs), other.dims_r() - ) + f"dimension on specified qargs{self.dims_r(qargs)} != {other.dims_r()}" ) elif self != other: if self._dim_l != other._dim_l: raise QiskitError( "Cannot add shapes width different total left " - "dimension {} != {}".format(self._dim_l, other._dim_l) + f"dimension {self._dim_l} != {other._dim_l}" ) if self._dim_r != other._dim_r: raise QiskitError( "Cannot add shapes width different total right " - "dimension {} != {}".format(self._dim_r, other._dim_r) + f"dimension {self._dim_r} != {other._dim_r}" ) return self diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 016e337f082..a4e93f36480 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -128,12 +128,9 @@ def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED): def __repr__(self): prefix = "Operator(" pad = len(prefix) * " " - return "{}{},\n{}input_dims={}, output_dims={})".format( - prefix, - np.array2string(self.data, separator=", ", prefix=prefix), - pad, - self.input_dims(), - self.output_dims(), + return ( + f"{prefix}{np.array2string(self.data, separator=', ', prefix=prefix)},\n" + f"{pad}input_dims={self.input_dims()}, output_dims={self.output_dims()})" ) def __eq__(self, other): @@ -763,10 +760,8 @@ def _append_instruction(self, obj, qargs=None): raise QiskitError(f"Cannot apply Operation: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - 'Operation "{}" ' - "definition is {} but expected QuantumCircuit.".format( - obj.name, type(obj.definition) - ) + f'Operation "{obj.name}" ' + f"definition is {type(obj.definition)} but expected QuantumCircuit." ) if obj.definition.global_phase: dimension = 2**obj.num_qubits diff --git a/qiskit/quantum_info/operators/predicates.py b/qiskit/quantum_info/operators/predicates.py index 57b7df64f26..f432195cd57 100644 --- a/qiskit/quantum_info/operators/predicates.py +++ b/qiskit/quantum_info/operators/predicates.py @@ -22,6 +22,7 @@ def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT, props=None): + # pylint: disable-next=consider-using-f-string """Test if two arrays are equal. The final comparison is implemented using Numpy.allclose. See its diff --git a/qiskit/quantum_info/operators/symplectic/base_pauli.py b/qiskit/quantum_info/operators/symplectic/base_pauli.py index e43eca4aff2..38e471f0b0a 100644 --- a/qiskit/quantum_info/operators/symplectic/base_pauli.py +++ b/qiskit/quantum_info/operators/symplectic/base_pauli.py @@ -215,12 +215,12 @@ def commutes(self, other: BasePauli, qargs: list | None = None) -> np.ndarray: if qargs is not None and len(qargs) != other.num_qubits: raise QiskitError( "Number of qubits of other Pauli does not match number of " - "qargs ({} != {}).".format(other.num_qubits, len(qargs)) + f"qargs ({other.num_qubits} != {len(qargs)})." ) if qargs is None and self.num_qubits != other.num_qubits: raise QiskitError( "Number of qubits of other Pauli does not match the current " - "Pauli ({} != {}).".format(other.num_qubits, self.num_qubits) + f"Pauli ({other.num_qubits} != {self.num_qubits})." ) if qargs is not None: inds = list(qargs) @@ -262,15 +262,12 @@ def evolve( # Check dimension if qargs is not None and len(qargs) != other.num_qubits: raise QiskitError( - "Incorrect number of qubits for Clifford circuit ({} != {}).".format( - other.num_qubits, len(qargs) - ) + f"Incorrect number of qubits for Clifford circuit ({other.num_qubits} != {len(qargs)})." ) if qargs is None and self.num_qubits != other.num_qubits: raise QiskitError( - "Incorrect number of qubits for Clifford circuit ({} != {}).".format( - other.num_qubits, self.num_qubits - ) + f"Incorrect number of qubits for Clifford circuit " + f"({other.num_qubits} != {self.num_qubits})." ) # Evolve via Pauli @@ -571,9 +568,8 @@ def _append_circuit(self, circuit, qargs=None): raise QiskitError(f"Cannot apply Instruction: {gate.name}") if not isinstance(gate.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - gate.name, type(gate.definition) - ) + f"{gate.name} instruction definition is {type(gate.definition)};" + f" expected QuantumCircuit" ) circuit = gate.definition diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index 8187c1ee41e..867867eeb98 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -344,7 +344,7 @@ def delete(self, qubits: int | list) -> Pauli: if max(qubits) > self.num_qubits - 1: raise QiskitError( "Qubit index is larger than the number of qubits " - "({}>{}).".format(max(qubits), self.num_qubits - 1) + f"({max(qubits)}>{self.num_qubits - 1})." ) if len(qubits) == self.num_qubits: raise QiskitError("Cannot delete all qubits of Pauli") @@ -379,12 +379,12 @@ def insert(self, qubits: int | list, value: Pauli) -> Pauli: if len(qubits) != value.num_qubits: raise QiskitError( "Number of indices does not match number of qubits for " - "the inserted Pauli ({}!={})".format(len(qubits), value.num_qubits) + f"the inserted Pauli ({len(qubits)}!={value.num_qubits})" ) if max(qubits) > ret.num_qubits - 1: raise QiskitError( "Index is too larger for combined Pauli number of qubits " - "({}>{}).".format(max(qubits), ret.num_qubits - 1) + f"({max(qubits)}>{ret.num_qubits - 1})." ) # Qubit positions for original op self_qubits = [i for i in range(ret.num_qubits) if i not in qubits] diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index 3d348d23638..f2e408dd9bd 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -382,8 +382,8 @@ def delete(self, ind: int | list, qubit: bool = False) -> PauliList: if not qubit: if max(ind) >= len(self): raise QiskitError( - "Indices {} are not all less than the size" - " of the PauliList ({})".format(ind, len(self)) + f"Indices {ind} are not all less than the size" + f" of the PauliList ({len(self)})" ) z = np.delete(self._z, ind, axis=0) x = np.delete(self._x, ind, axis=0) @@ -394,8 +394,8 @@ def delete(self, ind: int | list, qubit: bool = False) -> PauliList: # Column (qubit) deletion if max(ind) >= self.num_qubits: raise QiskitError( - "Indices {} are not all less than the number of" - " qubits in the PauliList ({})".format(ind, self.num_qubits) + f"Indices {ind} are not all less than the number of" + f" qubits in the PauliList ({self.num_qubits})" ) z = np.delete(self._z, ind, axis=1) x = np.delete(self._x, ind, axis=1) @@ -432,8 +432,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: if not qubit: if ind > size: raise QiskitError( - "Index {} is larger than the number of rows in the" - " PauliList ({}).".format(ind, size) + f"Index {ind} is larger than the number of rows in the" f" PauliList ({size})." ) base_z = np.insert(self._z, ind, value._z, axis=0) base_x = np.insert(self._x, ind, value._x, axis=0) @@ -443,8 +442,8 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: # Column insertion if ind > self.num_qubits: raise QiskitError( - "Index {} is greater than number of qubits" - " in the PauliList ({})".format(ind, self.num_qubits) + f"Index {ind} is greater than number of qubits" + f" in the PauliList ({self.num_qubits})" ) if len(value) == 1: # Pad blocks to correct size @@ -461,7 +460,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: raise QiskitError( "Input PauliList must have a single row, or" " the same number of rows as the Pauli Table" - " ({}).".format(size) + f" ({size})." ) # Build new array by blocks z = np.hstack([self.z[:, :ind], value_z, self.z[:, ind:]]) diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 6204189be44..91d8d82deca 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -172,7 +172,7 @@ def __init__( if self._coeffs.shape != (self._pauli_list.size,): raise QiskitError( "coeff vector is incorrect shape for number" - " of Paulis {} != {}".format(self._coeffs.shape, self._pauli_list.size) + f" of Paulis {self._coeffs.shape} != {self._pauli_list.size}" ) # Initialize LinearOp super().__init__(num_qubits=self._pauli_list.num_qubits) @@ -186,11 +186,9 @@ def __array__(self, dtype=None, copy=None): def __repr__(self): prefix = "SparsePauliOp(" pad = len(prefix) * " " - return "{}{},\n{}coeffs={})".format( - prefix, - self.paulis.to_labels(), - pad, - np.array2string(self.coeffs, separator=", "), + return ( + f"{prefix}{self.paulis.to_labels()},\n{pad}" + f"coeffs={np.array2string(self.coeffs, separator=', ')})" ) def __eq__(self, other): diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 1c66d8bcf5c..7826e3f22fe 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -123,11 +123,9 @@ def __eq__(self, other): def __repr__(self): prefix = "DensityMatrix(" pad = len(prefix) * " " - return "{}{},\n{}dims={})".format( - prefix, - np.array2string(self._data, separator=", ", prefix=prefix), - pad, - self._op_shape.dims_l(), + return ( + f"{prefix}{np.array2string(self._data, separator=', ', prefix=prefix)},\n" + f"{pad}dims={self._op_shape.dims_l()})" ) @property @@ -771,9 +769,8 @@ def _append_instruction(self, other, qargs=None): raise QiskitError(f"Cannot apply Instruction: {other.name}") if not isinstance(other.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - other.name, type(other.definition) - ) + f"{other.name} instruction definition is {type(other.definition)};" + f" expected QuantumCircuit" ) qubit_indices = {bit: idx for idx, bit in enumerate(other.definition.qubits)} for instruction in other.definition: diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index df39ba42f91..7fa14eaac9d 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -117,11 +117,9 @@ def __eq__(self, other): def __repr__(self): prefix = "Statevector(" pad = len(prefix) * " " - return "{}{},\n{}dims={})".format( - prefix, - np.array2string(self._data, separator=", ", prefix=prefix), - pad, - self._op_shape.dims_l(), + return ( + f"{prefix}{np.array2string(self._data, separator=', ', prefix=prefix)},\n{pad}" + f"dims={self._op_shape.dims_l()})" ) @property @@ -940,9 +938,7 @@ def _evolve_instruction(statevec, obj, qargs=None): raise QiskitError(f"Cannot apply Instruction: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - obj.name, type(obj.definition) - ) + f"{obj.name} instruction definition is {type(obj.definition)}; expected QuantumCircuit" ) if obj.definition.global_phase: diff --git a/qiskit/result/counts.py b/qiskit/result/counts.py index 8b90ff0f042..8168a3d2190 100644 --- a/qiskit/result/counts.py +++ b/qiskit/result/counts.py @@ -130,7 +130,7 @@ def most_frequent(self): max_values_counts = [x[0] for x in self.items() if x[1] == max_value] if len(max_values_counts) != 1: raise exceptions.QiskitError( - "Multiple values have the same maximum counts: %s" % ",".join(max_values_counts) + f"Multiple values have the same maximum counts: {','.join(max_values_counts)}" ) return max_values_counts[0] diff --git a/qiskit/result/mitigation/correlated_readout_mitigator.py b/qiskit/result/mitigation/correlated_readout_mitigator.py index 06cc89b4c52..99e6f9ae414 100644 --- a/qiskit/result/mitigation/correlated_readout_mitigator.py +++ b/qiskit/result/mitigation/correlated_readout_mitigator.py @@ -54,8 +54,8 @@ def __init__(self, assignment_matrix: np.ndarray, qubits: Optional[Iterable[int] else: if len(qubits) != matrix_qubits_num: raise QiskitError( - "The number of given qubits ({}) is different than the number of " - "qubits inferred from the matrices ({})".format(len(qubits), matrix_qubits_num) + f"The number of given qubits ({len(qubits)}) is different than the number of " + f"qubits inferred from the matrices ({matrix_qubits_num})" ) self._qubits = qubits self._num_qubits = len(self._qubits) diff --git a/qiskit/result/mitigation/local_readout_mitigator.py b/qiskit/result/mitigation/local_readout_mitigator.py index ad71911c2d7..197c3f00d9b 100644 --- a/qiskit/result/mitigation/local_readout_mitigator.py +++ b/qiskit/result/mitigation/local_readout_mitigator.py @@ -68,8 +68,8 @@ def __init__( else: if len(qubits) != len(assignment_matrices): raise QiskitError( - "The number of given qubits ({}) is different than the number of qubits " - "inferred from the matrices ({})".format(len(qubits), len(assignment_matrices)) + f"The number of given qubits ({len(qubits)}) is different than the number of qubits " + f"inferred from the matrices ({len(assignment_matrices)})" ) self._qubits = qubits self._num_qubits = len(self._qubits) diff --git a/qiskit/result/mitigation/utils.py b/qiskit/result/mitigation/utils.py index 26b5ee37348..823e2b69a6c 100644 --- a/qiskit/result/mitigation/utils.py +++ b/qiskit/result/mitigation/utils.py @@ -120,9 +120,7 @@ def marganalize_counts( clbits_len = len(clbits) if not clbits is None else 0 if clbits_len not in (0, qubits_len): raise QiskitError( - "Num qubits ({}) does not match number of clbits ({}).".format( - qubits_len, clbits_len - ) + f"Num qubits ({qubits_len}) does not match number of clbits ({clbits_len})." ) counts = marginal_counts(counts, clbits) if clbits is None and qubits is not None: diff --git a/qiskit/result/models.py b/qiskit/result/models.py index 99281019671..83d9e4e78d5 100644 --- a/qiskit/result/models.py +++ b/qiskit/result/models.py @@ -66,8 +66,7 @@ def __repr__(self): string_list = [] for field in self._data_attributes: string_list.append(f"{field}={getattr(self, field)}") - out = "ExperimentResultData(%s)" % ", ".join(string_list) - return out + return f"ExperimentResultData({', '.join(string_list)})" def to_dict(self): """Return a dictionary format representation of the ExperimentResultData @@ -157,23 +156,21 @@ def __init__( self._metadata.update(kwargs) def __repr__(self): - out = "ExperimentResult(shots={}, success={}, meas_level={}, data={}".format( - self.shots, - self.success, - self.meas_level, - self.data, + out = ( + f"ExperimentResult(shots={self.shots}, success={self.success}," + f" meas_level={self.meas_level}, data={self.data}" ) if hasattr(self, "header"): - out += ", header=%s" % self.header + out += f", header={self.header}" if hasattr(self, "status"): - out += ", status=%s" % self.status + out += f", status={self.status}" if hasattr(self, "seed"): - out += ", seed=%s" % self.seed + out += f", seed={self.seed}" if hasattr(self, "meas_return"): - out += ", meas_return=%s" % self.meas_return + out += f", meas_return={self.meas_return}" for key, value in self._metadata.items(): if isinstance(value, str): - value_str = "'%s'" % value + value_str = f"'{value}'" else: value_str = repr(value) out += f", {key}={value_str}" diff --git a/qiskit/result/result.py b/qiskit/result/result.py index c1792de56ae..7df36578516 100644 --- a/qiskit/result/result.py +++ b/qiskit/result/result.py @@ -69,21 +69,14 @@ def __init__( def __repr__(self): out = ( - "Result(backend_name='%s', backend_version='%s', qobj_id='%s', " - "job_id='%s', success=%s, results=%s" - % ( - self.backend_name, - self.backend_version, - self.qobj_id, - self.job_id, - self.success, - self.results, - ) + f"Result(backend_name='{self.backend_name}', backend_version='{self.backend_version}'," + f" qobj_id='{self.qobj_id}', job_id='{self.job_id}', success={self.success}," + f" results={self.results}" ) out += f", date={self.date}, status={self.status}, header={self.header}" for key, value in self._metadata.items(): if isinstance(value, str): - value_str = "'%s'" % value + value_str = f"'{value}'" else: value_str = repr(value) out += f", {key}={value_str}" @@ -236,10 +229,10 @@ def get_memory(self, experiment=None): except KeyError as ex: raise QiskitError( - 'No memory for experiment "{}". ' + f'No memory for experiment "{repr(experiment)}". ' "Please verify that you either ran a measurement level 2 job " 'with the memory flag set, eg., "memory=True", ' - "or a measurement level 0/1 job.".format(repr(experiment)) + "or a measurement level 0/1 job." ) from ex def get_counts(self, experiment=None): @@ -377,14 +370,14 @@ def _get_experiment(self, key=None): ] if len(exp) == 0: - raise QiskitError('Data for experiment "%s" could not be found.' % key) + raise QiskitError(f'Data for experiment "{key}" could not be found.') if len(exp) == 1: exp = exp[0] else: warnings.warn( - 'Result object contained multiple results matching name "%s", ' + f'Result object contained multiple results matching name "{key}", ' "only first match will be returned. Use an integer index to " - "retrieve results for all entries." % key + "retrieve results for all entries." ) exp = exp[0] diff --git a/qiskit/scheduler/lowering.py b/qiskit/scheduler/lowering.py index fa622b205d3..f0fb33957d9 100644 --- a/qiskit/scheduler/lowering.py +++ b/qiskit/scheduler/lowering.py @@ -145,9 +145,9 @@ def get_measure_schedule(qubit_mem_slots: Dict[int, int]) -> CircuitPulseDef: elif isinstance(instruction.operation, Measure): if len(inst_qubits) != 1 and len(instruction.clbits) != 1: raise QiskitError( - "Qubit '{}' or classical bit '{}' errored because the " + f"Qubit '{inst_qubits}' or classical bit '{instruction.clbits}' errored because the " "circuit Measure instruction only takes one of " - "each.".format(inst_qubits, instruction.clbits) + "each." ) qubit_mem_slots[inst_qubits[0]] = clbit_indices[instruction.clbits[0]] else: diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py index 5063577ed65..699523a7e75 100644 --- a/qiskit/synthesis/linear/cnot_synth.py +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -53,8 +53,7 @@ def synth_cnot_count_full_pmh( """ if not isinstance(state, (list, np.ndarray)): raise QiskitError( - "state should be of type list or numpy.ndarray, " - "but was of the type {}".format(type(state)) + f"state should be of type list or numpy.ndarray, but was of the type {type(state)}" ) state = np.array(state) # Synthesize lower triangular part diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 41ba75c6b23..26a5b52521b 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -116,7 +116,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): if deviation > 1.0e-13: raise QiskitError( "decompose_two_qubit_product_gate: decomposition failed: " - "deviation too large: {}".format(deviation) + f"deviation too large: {deviation}" ) return L, R, phase diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 614e166050e..27f98da68e7 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -101,7 +101,7 @@ def add_physical_qubit(self, physical_qubit): raise CouplingError("Physical qubits should be integers.") if physical_qubit in self.physical_qubits: raise CouplingError( - "The physical qubit %s is already in the coupling graph" % physical_qubit + f"The physical qubit {physical_qubit} is already in the coupling graph" ) self.graph.add_node(physical_qubit) self._dist_matrix = None # invalidate @@ -188,9 +188,9 @@ def distance(self, physical_qubit1, physical_qubit2): CouplingError: if the qubits do not exist in the CouplingMap """ if physical_qubit1 >= self.size(): - raise CouplingError("%s not in coupling graph" % physical_qubit1) + raise CouplingError(f"{physical_qubit1} not in coupling graph") if physical_qubit2 >= self.size(): - raise CouplingError("%s not in coupling graph" % physical_qubit2) + raise CouplingError(f"{physical_qubit2} not in coupling graph") self.compute_distance_matrix() res = self._dist_matrix[physical_qubit1, physical_qubit2] if res == math.inf: diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 1bebc7b84dc..4117e2987bb 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -98,8 +98,8 @@ def order_based_on_type(value1, value2): virtual = value1 else: raise LayoutError( - "The map (%s -> %s) has to be a (Bit -> integer)" - " or the other way around." % (type(value1), type(value2)) + f"The map ({type(value1)} -> {type(value2)}) has to be a (Bit -> integer)" + " or the other way around." ) return virtual, physical @@ -137,7 +137,7 @@ def __delitem__(self, key): else: raise LayoutError( "The key to remove should be of the form" - " Qubit or integer) and %s was provided" % (type(key),) + f" Qubit or integer) and {type(key)} was provided" ) def __len__(self): diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index bcac1f5d4fa..936613744b8 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -302,9 +302,7 @@ def _replace_node(self, dag, node, instr_map): if len(node.op.params) != len(target_params): raise TranspilerError( "Translation num_params not equal to op num_params." - "Op: {} {} Translation: {}\n{}".format( - node.op.params, node.op.name, target_params, target_dag - ) + f"Op: {node.op.params} {node.op.name} Translation: {target_params}\n{target_dag}" ) if node.op.params: parameter_map = dict(zip(target_params, node.op.params)) diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 701e87dd9cd..73e1d4ac548 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -78,7 +78,7 @@ def run(self, dag): continue raise QiskitError( "Cannot unroll all 3q or more gates. " - "No rule to expand instruction %s." % node.op.name + f"No rule to expand instruction {node.op.name}." ) decomposition = circuit_to_dag(node.op.definition, copy_operations=False) decomposition = self.run(decomposition) # recursively unroll diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index a54e4bfcb00..99bf95147ae 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -95,9 +95,9 @@ def run(self, dag): if unrolled is None: # opaque node raise QiskitError( - "Cannot unroll the circuit to the given basis, %s. " - "Instruction %s not found in equivalence library " - "and no rule found to expand." % (str(self._basis_gates), node.op.name) + f"Cannot unroll the circuit to the given basis, {str(self._basis_gates)}. " + f"Instruction {node.op.name} not found in equivalence library " + "and no rule found to expand." ) decomposition = circuit_to_dag(unrolled, copy_operations=False) diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 4cb576a23bc..c153c3eeef3 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -204,7 +204,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: xgate = self._inst_map.get("x", qubits[0]) with builder.build( - default_alignment="sequential", name="rzx(%.3f)" % theta + default_alignment="sequential", name=f"rzx({theta:.3f})" ) as rzx_theta_native: for cr_tone, comp_tone in zip(cr_tones, comp_tones): with builder.align_left(): @@ -230,7 +230,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche builder.call(szt, name="szt") with builder.build( - default_alignment="sequential", name="rzx(%.3f)" % theta + default_alignment="sequential", name=f"rzx({theta:.3f})" ) as rzx_theta_flip: builder.call(hadamard, name="hadamard") for cr_tone, comp_tone in zip(cr_tones, comp_tones): @@ -297,7 +297,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche # RZXCalibrationNoEcho only good for forward CR direction if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with builder.build(default_alignment="left", name="rzx(%.3f)" % theta) as rzx_theta: + with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) self.rescale_cr_inst(comp_tones[0], 2 * theta) # Placeholder to make pulse gate work diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index 958f53ef057..f5523432c26 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -53,8 +53,8 @@ def __init__(self, gates_to_cancel: List[Union[Gate, Tuple[Gate, Gate]]]): ) else: raise TranspilerError( - "InverseCancellation pass does not take input type {}. Input must be" - " a Gate.".format(type(gates)) + f"InverseCancellation pass does not take input type {type(gates)}. Input must be" + " a Gate." ) self.self_inverse_gates = [] diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py index 9370fe7409f..f8302b9232c 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py @@ -308,7 +308,7 @@ def run(self, dag): if "u3" in self.basis: new_op = U3Gate(*right_parameters) else: - raise TranspilerError("It was not possible to use the basis %s" % self.basis) + raise TranspilerError(f"It was not possible to use the basis {self.basis}") dag.global_phase += right_global_phase diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 28ae67b321c..acb23f39ab0 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -218,7 +218,7 @@ def run(self, dag): elif self.heuristic == "decay": heuristic = Heuristic.Decay else: - raise TranspilerError("Heuristic %s not recognized." % self.heuristic) + raise TranspilerError(f"Heuristic {self.heuristic} not recognized.") disjoint_utils.require_layout_isolated_to_component( dag, self.coupling_map if self.target is None else self.target ) diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index ec7ea814913..3732802b770 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -357,9 +357,7 @@ def _mapper(self, circuit_graph, coupling_graph, trials=20): # Give up if we fail again if not success_flag: - raise TranspilerError( - "swap mapper failed: " + "layer %d, sublayer %d" % (i, j) - ) + raise TranspilerError(f"swap mapper failed: layer {i}, sublayer {j}") # Update the record of qubit positions # for each inner iteration diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 150874a84c7..668d10f32bc 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -540,8 +540,8 @@ def _synthesize_op_using_plugins( if isinstance(plugin_specifier, str): if plugin_specifier not in hls_plugin_manager.method_names(op.name): raise TranspilerError( - "Specified method: %s not found in available plugins for %s" - % (plugin_specifier, op.name) + f"Specified method: {plugin_specifier} not found in available " + f"plugins for {op.name}" ) plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) else: diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index 61ddc71d131..437718ec27b 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -85,10 +85,8 @@ def _recurse(self, dag, wire_map) -> bool: and not dag.has_calibration_for(node) and (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) not in self.qargs ): - self.property_set["check_map_msg"] = "{}({}, {}) failed".format( - node.name, - wire_map[node.qargs[0]], - wire_map[node.qargs[1]], + self.property_set["check_map_msg"] = ( + f"{node.name}({wire_map[node.qargs[0]]}, {wire_map[node.qargs[1]]}) failed" ) return False return True diff --git a/qiskit/transpiler/passes/utils/error.py b/qiskit/transpiler/passes/utils/error.py index f2659ec052f..44420582c1a 100644 --- a/qiskit/transpiler/passes/utils/error.py +++ b/qiskit/transpiler/passes/utils/error.py @@ -43,7 +43,7 @@ def __init__(self, msg=None, action="raise"): if action in ["raise", "warn", "log"]: self.action = action else: - raise TranspilerError("Unknown action: %s" % action) + raise TranspilerError(f"Unknown action: {action}") def run(self, _): """Run the Error pass on `dag`.""" @@ -66,4 +66,4 @@ def run(self, _): logger = logging.getLogger(__name__) logger.info(msg) else: - raise TranspilerError("Unknown action: %s" % self.action) + raise TranspilerError(f"Unknown action: {self.action}") diff --git a/qiskit/transpiler/passes/utils/fixed_point.py b/qiskit/transpiler/passes/utils/fixed_point.py index fbef9d0a85e..a85a7a8e6e7 100644 --- a/qiskit/transpiler/passes/utils/fixed_point.py +++ b/qiskit/transpiler/passes/utils/fixed_point.py @@ -37,12 +37,12 @@ def __init__(self, property_to_check): def run(self, dag): """Run the FixedPoint pass on `dag`.""" current_value = self.property_set[self._property] - fixed_point_previous_property = "_fixed_point_previous_%s" % self._property + fixed_point_previous_property = f"_fixed_point_previous_{self._property}" if self.property_set[fixed_point_previous_property] is None: - self.property_set["%s_fixed_point" % self._property] = False + self.property_set[f"{self._property}_fixed_point"] = False else: fixed_point_reached = self.property_set[fixed_point_previous_property] == current_value - self.property_set["%s_fixed_point" % self._property] = fixed_point_reached + self.property_set[f"{self._property}_fixed_point"] = fixed_point_reached self.property_set[fixed_point_previous_property] = deepcopy(current_value) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 1b77e7dbd26..ec479f9e006 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -518,7 +518,7 @@ def generate_translation_passmanager( ), ] else: - raise TranspilerError("Invalid translation method %s." % method) + raise TranspilerError(f"Invalid translation method {method}.") return PassManager(unroll) @@ -557,7 +557,7 @@ def generate_scheduling( try: scheduling.append(scheduler[scheduling_method](instruction_durations, target=target)) except KeyError as ex: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex + raise TranspilerError(f"Invalid scheduling method {scheduling_method}.") from ex elif instruction_durations: # No scheduling. But do unit conversion for delays. def _contains_delay(property_set): diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 53daa4ccbe6..8805deece50 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -411,7 +411,7 @@ def add_instruction(self, instruction, properties=None, name=None): if properties is None: properties = {None: None} if instruction_name in self._gate_map: - raise AttributeError("Instruction %s is already in the target" % instruction_name) + raise AttributeError(f"Instruction {instruction_name} is already in the target") self._gate_name_map[instruction_name] = instruction if is_class: qargs_val = {None: None} @@ -1062,7 +1062,7 @@ def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): for qargs, properties in self._gate_map[two_q_gate].items(): if len(qargs) != 2: raise ValueError( - "Specified two_q_gate: %s is not a 2 qubit instruction" % two_q_gate + f"Specified two_q_gate: {two_q_gate} is not a 2 qubit instruction" ) coupling_graph.add_edge(*qargs, {two_q_gate: properties}) cmap = CouplingMap() diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 73a68eb6bfd..0ca52fc5c8c 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -63,9 +63,9 @@ def read_config_file(self): if circuit_drawer: if circuit_drawer not in ["text", "mpl", "latex", "latex_source", "auto"]: raise exceptions.QiskitUserConfigError( - "%s is not a valid circuit drawer backend. Must be " + f"{circuit_drawer} is not a valid circuit drawer backend. Must be " "either 'text', 'mpl', 'latex', 'latex_source', or " - "'auto'." % circuit_drawer + "'auto'." ) self.settings["circuit_drawer"] = circuit_drawer @@ -96,8 +96,8 @@ def read_config_file(self): if circuit_mpl_style: if not isinstance(circuit_mpl_style, str): warn( - "%s is not a valid mpl circuit style. Must be " - "a text string. Will not load style." % circuit_mpl_style, + f"{circuit_mpl_style} is not a valid mpl circuit style. Must be " + "a text string. Will not load style.", UserWarning, 2, ) @@ -112,8 +112,8 @@ def read_config_file(self): for path in cpath_list: if not os.path.exists(os.path.expanduser(path)): warn( - "%s is not a valid circuit mpl style path." - " Correct the path in ~/.qiskit/settings.conf." % path, + f"{path} is not a valid circuit mpl style path." + " Correct the path in ~/.qiskit/settings.conf.", UserWarning, 2, ) diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index a1672dc1676..146de9d32de 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -345,8 +345,8 @@ def check_clbit_in_inst(circuit, cregbundle): ) else: raise VisualizationError( - "Invalid output type %s selected. The only valid choices " - "are text, latex, latex_source, and mpl" % output + f"Invalid output type {output} selected. The only valid choices " + "are text, latex, latex_source, and mpl" ) if image and interactive: image.show() diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index 9341126bcd1..4cb233277c0 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -415,7 +415,7 @@ def _build_latex_array(self): cwire_list = [] if len(wire_list) == 1 and not node.cargs: - self._latex[wire_list[0]][column] = "\\gate{%s}" % gate_text + self._latex[wire_list[0]][column] = f"\\gate{{{gate_text}}}" elif isinstance(op, ControlledGate): num_cols_op = self._build_ctrl_gate(op, gate_text, wire_list, column) @@ -443,20 +443,20 @@ def _build_multi_gate(self, op, gate_text, wire_list, cwire_list, col): self._latex[wire_min][col] = ( f"\\multigate{{{wire_max - wire_min}}}{{{gate_text}}}_" + "<" * (len(str(wire_ind)) + 2) - + "{%s}" % wire_ind + + f"{{{wire_ind}}}" ) for wire in range(wire_min + 1, wire_max + 1): if wire < cwire_start: - ghost_box = "\\ghost{%s}" % gate_text + ghost_box = f"\\ghost{{{gate_text}}}" if wire in wire_list: wire_ind = wire_list.index(wire) else: - ghost_box = "\\cghost{%s}" % gate_text + ghost_box = f"\\cghost{{{gate_text}}}" if wire in cwire_list: wire_ind = cwire_list.index(wire) if wire in wire_list + cwire_list: self._latex[wire][col] = ( - ghost_box + "_" + "<" * (len(str(wire_ind)) + 2) + "{%s}" % wire_ind + ghost_box + "_" + "<" * (len(str(wire_ind)) + 2) + f"{{{wire_ind}}}" ) else: self._latex[wire][col] = ghost_box @@ -484,7 +484,7 @@ def _build_ctrl_gate(self, op, gate_text, wire_list, col): elif isinstance(op.base_gate, (U1Gate, PhaseGate)): num_cols_op = self._build_symmetric_gate(op, gate_text, wire_list, col) else: - self._latex[wireqargs[0]][col] = "\\gate{%s}" % gate_text + self._latex[wireqargs[0]][col] = f"\\gate{{{gate_text}}}" else: # Treat special cases of swap and rzz gates if isinstance(op.base_gate, (SwapGate, RZZGate)): @@ -527,7 +527,7 @@ def _build_symmetric_gate(self, op, gate_text, wire_list, col): ) self._latex[wire_last][col] = "\\control \\qw" # Put side text to the right between bottom wire in wire_list and the one above it - self._latex[wire_max - 1][col + 1] = "\\dstick{\\hspace{2.0em}%s} \\qw" % gate_text + self._latex[wire_max - 1][col + 1] = f"\\dstick{{\\hspace{{2.0em}}{gate_text}}} \\qw" return 4 # num_cols for side text gates def _build_measure(self, node, col): @@ -544,11 +544,9 @@ def _build_measure(self, node, col): idx_str = str(self._circuit.find_bit(node.cargs[0]).registers[0][1]) else: wire2 = self._wire_map[node.cargs[0]] - - self._latex[wire2][col] = "\\dstick{_{_{\\hspace{%sem}%s}}} \\cw \\ar @{<=} [-%s,0]" % ( - cond_offset, - idx_str, - str(wire2 - wire1), + self._latex[wire2][col] = ( + f"\\dstick{{_{{_{{\\hspace{{{cond_offset}em}}{idx_str}}}}}}} " + f"\\cw \\ar @{{<=}} [-{str(wire2 - wire1)},0]" ) else: wire2 = self._wire_map[node.cargs[0]] @@ -573,7 +571,7 @@ def _build_barrier(self, node, col): if node.op.label is not None: pos = indexes[0] label = node.op.label.replace(" ", "\\,") - self._latex[pos][col] = "\\cds{0}{^{\\mathrm{%s}}}" % label + self._latex[pos][col] = f"\\cds{{0}}{{^{{\\mathrm{{{label}}}}}}}" def _add_controls(self, wire_list, ctrlqargs, ctrl_state, col): """Add one or more controls to a gate""" @@ -615,11 +613,10 @@ def _add_condition(self, op, wire_list, col): ) gap = cwire - max(wire_list) control = "\\control" if op.condition[1] else "\\controlo" - self._latex[cwire][col] = f"{control}" + " \\cw^(%s){^{\\mathtt{%s}}} \\cwx[-%s]" % ( - meas_offset, - label, - str(gap), - ) + self._latex[cwire][ + col + ] = f"{control} \\cw^({meas_offset}){{^{{\\mathtt{{{label}}}}}}} \\cwx[-{str(gap)}]" + # If condition is a register and cregbundle is false else: # First sort the val_bits in the order of the register bits in the circuit diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 0076073fb8e..9c4fa25309f 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -371,7 +371,7 @@ def draw(self, filename=None, verbose=False): # Once the scaling factor has been determined, the global phase, register names # and numbers, wires, and gates are drawn if self._global_phase: - plt_mod.text(xl, yt, "Global Phase: %s" % pi_check(self._global_phase, output="mpl")) + plt_mod.text(xl, yt, f"Global Phase: {pi_check(self._global_phase, output='mpl')}") self._draw_regs_wires(num_folds, xmax, max_x_index, qubits_dict, clbits_dict, glob_data) self._draw_ops( self._nodes, diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index bec1ccf4a3e..e9b7aa819e9 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -759,7 +759,7 @@ def _repr_html_(self): "background: #fff0;" "line-height: 1.1;" 'font-family: "Courier New",Courier,monospace">' - "%s" % self.single_string() + f"{self.single_string()}" ) def __repr__(self): @@ -780,8 +780,9 @@ def single_string(self): ) except (UnicodeEncodeError, UnicodeDecodeError): warn( - "The encoding %s has a limited charset. Consider a different encoding in your " - "environment. UTF-8 is being used instead" % self.encoding, + f"The encoding {self.encoding} has a limited charset." + " Consider a different encoding in your " + "environment. UTF-8 is being used instead", RuntimeWarning, ) self.encoding = "utf-8" @@ -861,7 +862,7 @@ def lines(self, line_length=None): lines = [] if self.global_phase: - lines.append("global phase: %s" % pi_check(self.global_phase, ndigits=5)) + lines.append(f"global phase: {pi_check(self.global_phase, ndigits=5)}") for layer_group in layer_groups: wires = list(zip(*layer_group)) @@ -1168,7 +1169,7 @@ def add_connected_gate(node, gates, layer, current_cons, gate_wire_map): elif isinstance(op, RZZGate): # rzz - connection_label = "ZZ%s" % params + connection_label = f"ZZ{params}" gates = [Bullet(conditional=conditional), Bullet(conditional=conditional)] add_connected_gate(node, gates, layer, current_cons, gate_wire_map) @@ -1211,7 +1212,7 @@ def add_connected_gate(node, gates, layer, current_cons, gate_wire_map): add_connected_gate(node, gates, layer, current_cons, gate_wire_map) elif base_gate.name == "rzz": # crzz - connection_label = "ZZ%s" % params + connection_label = f"ZZ{params}" gates += [Bullet(conditional=conditional), Bullet(conditional=conditional)] elif len(rest) > 1: top_connect = "┴" if controlled_top else None diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index 73b9c30f6dc..ad2fca6e9bc 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -152,7 +152,7 @@ def node_attr_func(node): n["fillcolor"] = "lightblue" return n else: - raise VisualizationError("Unrecognized style %s for the dag_drawer." % style) + raise VisualizationError(f"Unrecognized style {style} for the dag_drawer.") edge_attr_func = None @@ -197,7 +197,7 @@ def node_attr_func(node): n["fillcolor"] = "red" return n else: - raise VisualizationError("Invalid style %s" % style) + raise VisualizationError(f"Invalid style {style}") def edge_attr_func(edge): e = {} diff --git a/qiskit/visualization/pulse_v2/core.py b/qiskit/visualization/pulse_v2/core.py index d60f2db030d..20686f6fb4f 100644 --- a/qiskit/visualization/pulse_v2/core.py +++ b/qiskit/visualization/pulse_v2/core.py @@ -220,7 +220,7 @@ def load_program( elif isinstance(program, (pulse.Waveform, pulse.SymbolicPulse)): self._waveform_loader(program) else: - raise VisualizationError("Data type %s is not supported." % type(program)) + raise VisualizationError(f"Data type {type(program)} is not supported.") # update time range self.set_time_range(0, program.duration, seconds=False) diff --git a/qiskit/visualization/pulse_v2/generators/frame.py b/qiskit/visualization/pulse_v2/generators/frame.py index 394f4b4aaf8..8b71b8596bb 100644 --- a/qiskit/visualization/pulse_v2/generators/frame.py +++ b/qiskit/visualization/pulse_v2/generators/frame.py @@ -264,10 +264,9 @@ def gen_raw_operand_values_compact( freq_sci_notation = "0.0" else: abs_freq = np.abs(data.frame.freq) - freq_sci_notation = "{base:.1f}e{exp:d}".format( - base=data.frame.freq / (10 ** int(np.floor(np.log10(abs_freq)))), - exp=int(np.floor(np.log10(abs_freq))), - ) + base = data.frame.freq / (10 ** int(np.floor(np.log10(abs_freq)))) + exponent = int(np.floor(np.log10(abs_freq))) + freq_sci_notation = f"{base:.1f}e{exponent:d}" frame_info = f"{data.frame.phase:.2f}\n{freq_sci_notation}" text = drawings.TextData( diff --git a/qiskit/visualization/pulse_v2/generators/waveform.py b/qiskit/visualization/pulse_v2/generators/waveform.py index b0d90b895c7..e770f271c45 100644 --- a/qiskit/visualization/pulse_v2/generators/waveform.py +++ b/qiskit/visualization/pulse_v2/generators/waveform.py @@ -203,11 +203,10 @@ def gen_ibmq_latex_waveform_name( if frac.numerator == 1: angle = rf"\pi/{frac.denominator:d}" else: - angle = r"{num:d}/{denom:d} \pi".format( - num=frac.numerator, denom=frac.denominator - ) + angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" else: # single qubit pulse + # pylint: disable-next=consider-using-f-string op_name = r"{{\rm {}}}".format(match_dict["op"]) angle_val = match_dict["angle"] if angle_val is None: @@ -217,9 +216,7 @@ def gen_ibmq_latex_waveform_name( if frac.numerator == 1: angle = rf"\pi/{frac.denominator:d}" else: - angle = r"{num:d}/{denom:d} \pi".format( - num=frac.numerator, denom=frac.denominator - ) + angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" latex_name = rf"{op_name}({sign}{angle})" else: latex_name = None @@ -490,7 +487,7 @@ def _draw_opaque_waveform( fill_objs.append(box_obj) # parameter name - func_repr = "{func}({params})".format(func=pulse_shape, params=", ".join(pnames)) + func_repr = f"{pulse_shape}({', '.join(pnames)})" text_style = { "zorder": formatter["layer.annotate"], @@ -630,8 +627,7 @@ def _parse_waveform( meta.update(acq_data) else: raise VisualizationError( - "Unsupported instruction {inst} by " - "filled envelope.".format(inst=inst.__class__.__name__) + f"Unsupported instruction {inst.__class__.__name__} by " "filled envelope." ) meta.update( diff --git a/qiskit/visualization/pulse_v2/layouts.py b/qiskit/visualization/pulse_v2/layouts.py index 6b39dceaf56..13b42e394e9 100644 --- a/qiskit/visualization/pulse_v2/layouts.py +++ b/qiskit/visualization/pulse_v2/layouts.py @@ -373,11 +373,7 @@ def detail_title(program: Union[pulse.Waveform, pulse.Schedule], device: DrawerB # add program duration dt = device.dt * 1e9 if device.dt else 1.0 - title_str.append( - "Duration: {dur:.1f} {unit}".format( - dur=program.duration * dt, unit="ns" if device.dt else "dt" - ) - ) + title_str.append(f"Duration: {program.duration * dt:.1f} {'ns' if device.dt else 'dt'}") # add device name if device.backend_name != "no-backend": diff --git a/qiskit/visualization/pulse_v2/plotters/matplotlib.py b/qiskit/visualization/pulse_v2/plotters/matplotlib.py index e92a3418999..1788a125489 100644 --- a/qiskit/visualization/pulse_v2/plotters/matplotlib.py +++ b/qiskit/visualization/pulse_v2/plotters/matplotlib.py @@ -119,8 +119,7 @@ def draw(self): self.ax.add_patch(box) else: raise VisualizationError( - "Data {name} is not supported " - "by {plotter}".format(name=data, plotter=self.__class__.__name__) + f"Data {data} is not supported " f"by {self.__class__.__name__}" ) # axis break for pos in axis_config.axis_break_pos: diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index e862b0208e2..0e47a5fe6d7 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -971,10 +971,10 @@ def plot_state_qsphere( if show_state_phases: element_angle = (np.angle(state[i]) + (np.pi * 4)) % (np.pi * 2) if use_degrees: - element_text += "\n$%.1f^\\circ$" % (element_angle * 180 / np.pi) + element_text += f"\n${element_angle * 180 / np.pi:.1f}^\\circ$" else: element_angle = pi_check(element_angle, ndigits=3).replace("pi", "\\pi") - element_text += "\n$%s$" % (element_angle) + element_text += f"\n${element_angle}$" ax.text( xvalue_text, yvalue_text, @@ -1463,11 +1463,10 @@ def state_drawer(state, output=None, **drawer_args): return draw_func(state, **drawer_args) except KeyError as err: raise ValueError( - """'{}' is not a valid option for drawing {} objects. Please choose from: + f"""'{output}' is not a valid option for drawing {type(state).__name__} + objects. Please choose from: 'text', 'latex', 'latex_source', 'qsphere', 'hinton', - 'bloch', 'city' or 'paulivec'.""".format( - output, type(state).__name__ - ) + 'bloch', 'city' or 'paulivec'.""" ) from err diff --git a/qiskit/visualization/timeline/plotters/matplotlib.py b/qiskit/visualization/timeline/plotters/matplotlib.py index 126d0981fed..daae6fe2558 100644 --- a/qiskit/visualization/timeline/plotters/matplotlib.py +++ b/qiskit/visualization/timeline/plotters/matplotlib.py @@ -132,8 +132,7 @@ def draw(self): else: raise VisualizationError( - "Data {name} is not supported by {plotter}" - "".format(name=data, plotter=self.__class__.__name__) + f"Data {data} is not supported by {self.__class__.__name__}" ) def _time_bucket_outline( diff --git a/test/benchmarks/circuit_construction.py b/test/benchmarks/circuit_construction.py index 71c079476cd..6c6c8733d25 100644 --- a/test/benchmarks/circuit_construction.py +++ b/test/benchmarks/circuit_construction.py @@ -52,7 +52,7 @@ def time_circuit_copy(self, _, __): def build_parameterized_circuit(width, gates, param_count): - params = [Parameter("param-%s" % x) for x in range(param_count)] + params = [Parameter(f"param-{x}") for x in range(param_count)] param_iter = itertools.cycle(params) qr = QuantumRegister(width) diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 121ad7222f4..c1ece0230d3 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -167,7 +167,7 @@ def test_circuit_qasm_with_multiple_composite_circuits_with_same_name(self): my_gate_inst2_id = id(circuit.data[-1].operation) circuit.append(my_gate_inst3, [qr[0]]) my_gate_inst3_id = id(circuit.data[-1].operation) - + # pylint: disable-next=consider-using-f-string expected_qasm = """OPENQASM 2.0; include "qelib1.inc"; gate my_gate q0 {{ h q0; }} diff --git a/test/python/circuit/test_circuit_registers.py b/test/python/circuit/test_circuit_registers.py index 3de2a451877..7ef8751da0e 100644 --- a/test/python/circuit/test_circuit_registers.py +++ b/test/python/circuit/test_circuit_registers.py @@ -97,7 +97,7 @@ def test_numpy_array_of_registers(self): """Test numpy array of Registers . See https://github.com/Qiskit/qiskit-terra/issues/1898 """ - qrs = [QuantumRegister(2, name="q%s" % i) for i in range(5)] + qrs = [QuantumRegister(2, name=f"q{i}") for i in range(5)] qreg_array = np.array([], dtype=object, ndmin=1) qreg_array = np.append(qreg_array, qrs) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 4ac69278fd4..dbda9262f15 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -423,17 +423,15 @@ def test_repr_of_instructions(self): ins1 = Instruction("test_instruction", 3, 5, [0, 1, 2, 3]) self.assertEqual( repr(ins1), - "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - ins1.name, ins1.num_qubits, ins1.num_clbits, ins1.params - ), + f"Instruction(name='{ins1.name}', num_qubits={ins1.num_qubits}, " + f"num_clbits={ins1.num_clbits}, params={ins1.params})", ) ins2 = random_circuit(num_qubits=4, depth=4, measure=True).to_instruction() self.assertEqual( repr(ins2), - "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - ins2.name, ins2.num_qubits, ins2.num_clbits, ins2.params - ), + f"Instruction(name='{ins2.name}', num_qubits={ins2.num_qubits}, " + f"num_clbits={ins2.num_clbits}, params={ins2.params})", ) def test_instruction_condition_bits(self): diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index fd63057cff8..feab3002f58 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -58,8 +58,8 @@ def raise_if_parameter_table_invalid(circuit): if circuit_parameters != table_parameters: raise CircuitError( "Circuit/ParameterTable Parameter mismatch. " - "Circuit parameters: {}. " - "Table parameters: {}.".format(circuit_parameters, table_parameters) + f"Circuit parameters: {circuit_parameters}. " + f"Table parameters: {table_parameters}." ) # Assert parameter locations in table are present in circuit. @@ -75,16 +75,15 @@ def raise_if_parameter_table_invalid(circuit): if not isinstance(instr.params[param_index], ParameterExpression): raise CircuitError( "ParameterTable instruction does not have a " - "ParameterExpression at param_index {}: {}." - "".format(param_index, instr) + f"ParameterExpression at param_index {param_index}: {instr}." ) if parameter not in instr.params[param_index].parameters: raise CircuitError( "ParameterTable instruction parameters does " "not match ParameterTable key. Instruction " - "parameters: {} ParameterTable key: {}." - "".format(instr.params[param_index].parameters, parameter) + f"parameters: {instr.params[param_index].parameters}" + f" ParameterTable key: {parameter}." ) # Assert circuit has no other parameter locations other than those in table. @@ -99,8 +98,8 @@ def raise_if_parameter_table_invalid(circuit): ): raise CircuitError( "Found parameterized instruction not " - "present in table. Instruction: {} " - "param_index: {}".format(instruction.operation, param_index) + f"present in table. Instruction: {instruction.operation} " + f"param_index: {param_index}" ) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 14033e522c6..0fcff29e5b4 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -83,8 +83,8 @@ def raise_if_dagcircuit_invalid(dag): ] if edges_outside_wires: raise DAGCircuitError( - "multi_graph contains one or more edges ({}) " - "not found in DAGCircuit.wires ({}).".format(edges_outside_wires, dag.wires) + f"multi_graph contains one or more edges ({edges_outside_wires}) " + f"not found in DAGCircuit.wires ({dag.wires})." ) # Every wire should have exactly one input node and one output node. @@ -134,9 +134,7 @@ def raise_if_dagcircuit_invalid(dag): all_bits = node_qubits | node_clbits | node_cond_bits assert in_wires == all_bits, f"In-edge wires {in_wires} != node bits {all_bits}" - assert out_wires == all_bits, "Out-edge wires {} != node bits {}".format( - out_wires, all_bits - ) + assert out_wires == all_bits, f"Out-edge wires {out_wires} != node bits {all_bits}" class TestDagRegisters(QiskitTestCase): diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 101e35acc8e..d5c5507b3b8 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -105,7 +105,7 @@ def setUpClass(cls): ) def test_circuit_on_fake_backend_v2(self, backend, optimization_level): if not optionals.HAS_AER and backend.num_qubits > 20: - self.skipTest("Unable to run fake_backend %s without qiskit-aer" % backend.name) + self.skipTest(f"Unable to run fake_backend {backend.name} without qiskit-aer") job = backend.run( transpile( self.circuit, backend, seed_transpiler=42, optimization_level=optimization_level @@ -126,8 +126,7 @@ def test_circuit_on_fake_backend_v2(self, backend, optimization_level): def test_circuit_on_fake_backend(self, backend, optimization_level): if not optionals.HAS_AER and backend.configuration().num_qubits > 20: self.skipTest( - "Unable to run fake_backend %s without qiskit-aer" - % backend.configuration().backend_name + f"Unable to run fake_backend {backend.configuration().backend_name} without qiskit-aer" ) job = backend.run( transpile( @@ -202,7 +201,7 @@ def test_defaults_to_dict(self, backend): self.assertGreater(i, 1e6) self.assertGreater(i, 1e6) else: - self.skipTest("Backend %s does not have defaults" % backend) + self.skipTest(f"Backend {backend} does not have defaults") def test_delay_circuit(self): backend = Fake27QPulseV1() diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 3585efb9f64..043a9eca78b 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -150,7 +150,7 @@ def test_append_1_qubit_gate(self): "sx", "sxdg", ): - with self.subTest(msg="append gate %s" % gate_name): + with self.subTest(msg=f"append gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff = _append_operation(cliff, gate_name, [0]) value_table = cliff.tableau[:, :-1] @@ -170,7 +170,7 @@ def test_1_qubit_identity_relations(self): """Tests identity relations for 1-qubit gates""" for gate_name in ("x", "y", "z", "h"): - with self.subTest(msg="identity for gate %s" % gate_name): + with self.subTest(msg=f"identity for gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() cliff = _append_operation(cliff, gate_name, [0]) @@ -181,7 +181,7 @@ def test_1_qubit_identity_relations(self): inv_gates = ["sdg", "sinv", "w"] for gate_name, inv_gate in zip(gates, inv_gates): - with self.subTest(msg="identity for gate %s" % gate_name): + with self.subTest(msg=f"identity for gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() cliff = _append_operation(cliff, gate_name, [0]) @@ -203,7 +203,7 @@ def test_1_qubit_mult_relations(self): ] for rel in rels: - with self.subTest(msg="relation %s" % rel): + with self.subTest(msg=f"relation {rel}"): split_rel = rel.split() cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() @@ -227,7 +227,7 @@ def test_1_qubit_conj_relations(self): ] for rel in rels: - with self.subTest(msg="relation %s" % rel): + with self.subTest(msg=f"relation {rel}"): split_rel = rel.split() cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index 4f5c728604b..cf6ad3c3509 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -398,7 +398,7 @@ def test_to_dict(self): target = {} for i in range(2): for j in range(3): - key = "{1}{0}|{1}{0}".format(i, j) + key = f"{j}{i}|{j}{i}" target[key] = 2 * j + i + 1 self.assertDictAlmostEqual(target, rho.to_dict()) @@ -407,7 +407,7 @@ def test_to_dict(self): target = {} for i in range(2): for j in range(11): - key = "{1},{0}|{1},{0}".format(i, j) + key = f"{j},{i}|{j},{i}" target[key] = 2 * j + i + 1 self.assertDictAlmostEqual(target, vec.to_dict()) diff --git a/test/python/result/test_mitigators.py b/test/python/result/test_mitigators.py index d290fc8ed48..66662bb587e 100644 --- a/test/python/result/test_mitigators.py +++ b/test/python/result/test_mitigators.py @@ -140,22 +140,16 @@ def test_mitigation_improvement(self): self.assertLess( mitigated_error, unmitigated_error * 0.8, - "Mitigator {} did not improve circuit {} measurements".format( - mitigator, circuit_name - ), + f"Mitigator {mitigator} did not improve circuit {circuit_name} measurements", ) mitigated_stddev_upper_bound = mitigated_quasi_probs._stddev_upper_bound max_unmitigated_stddev = max(unmitigated_stddev.values()) self.assertGreaterEqual( mitigated_stddev_upper_bound, max_unmitigated_stddev, - "Mitigator {} on circuit {} gave stddev upper bound {} " - "while unmitigated stddev maximum is {}".format( - mitigator, - circuit_name, - mitigated_stddev_upper_bound, - max_unmitigated_stddev, - ), + f"Mitigator {mitigator} on circuit {circuit_name} gave stddev upper bound " + f"{mitigated_stddev_upper_bound} while unmitigated stddev maximum is " + f"{max_unmitigated_stddev}", ) def test_expectation_improvement(self): @@ -190,22 +184,15 @@ def test_expectation_improvement(self): self.assertLess( mitigated_error, unmitigated_error, - "Mitigator {} did not improve circuit {} expectation computation for diagonal {} " - "ideal: {}, unmitigated: {} mitigated: {}".format( - mitigator, - circuit_name, - diagonal, - ideal_expectation, - unmitigated_expectation, - mitigated_expectation, - ), + f"Mitigator {mitigator} did not improve circuit {circuit_name} expectation " + f"computation for diagonal {diagonal} ideal: {ideal_expectation}, unmitigated:" + f" {unmitigated_expectation} mitigated: {mitigated_expectation}", ) self.assertGreaterEqual( mitigated_stddev, unmitigated_stddev, - "Mitigator {} did not increase circuit {} the standard deviation".format( - mitigator, circuit_name - ), + f"Mitigator {mitigator} did not increase circuit {circuit_name} the" + f" standard deviation", ) def test_clbits_parameter(self): @@ -228,7 +215,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly marganalize for qubits 1,2".format(mitigator), + f"Mitigator {mitigator} did not correctly marganalize for qubits 1,2", ) mitigated_probs_02 = ( @@ -240,7 +227,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly marganalize for qubits 0,2".format(mitigator), + f"Mitigator {mitigator} did not correctly marganalize for qubits 0,2", ) def test_qubits_parameter(self): @@ -264,7 +251,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 0, 1, 2".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 0, 1, 2", ) mitigated_probs_210 = ( @@ -276,7 +263,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 2, 1, 0".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 2, 1, 0", ) mitigated_probs_102 = ( @@ -288,7 +275,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 1, 0, 2".format(mitigator), + "Mitigator {mitigator} did not correctly handle qubit order 1, 0, 2", ) def test_repeated_qubits_parameter(self): @@ -311,7 +298,7 @@ def test_repeated_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 2,1,0".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 2,1,0", ) # checking qubit order 2,1,0 should not "overwrite" the default 0,1,2 @@ -324,9 +311,8 @@ def test_repeated_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 0,1,2 (the expected default)".format( - mitigator - ), + f"Mitigator {mitigator} did not correctly handle qubit order 0,1,2 " + f"(the expected default)", ) def test_qubits_subset_parameter(self): @@ -350,7 +336,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit subset".format(mitigator), + "Mitigator {mitigator} did not correctly handle qubit subset", ) mitigated_probs_6 = ( @@ -362,7 +348,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit subset".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit subset", ) diagonal = str2diag("ZZ") ideal_expectation = 0 @@ -373,7 +359,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.1, - "Mitigator {} did not improve circuit expectation".format(mitigator), + f"Mitigator {mitigator} did not improve circuit expectation", ) def test_from_backend(self): diff --git a/test/python/synthesis/aqc/fast_gradient/test_layer1q.py b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py index 43b164c4225..d2c5108391f 100644 --- a/test/python/synthesis/aqc/fast_gradient/test_layer1q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py @@ -62,7 +62,7 @@ def test_layer1q_matrix(self): # T == P^t @ G @ P. err = tut.relative_error(t_mat, iden[perm].T @ g_mat @ iden[perm]) - self.assertLess(err, eps, "err = {:0.16f}".format(err)) + self.assertLess(err, eps, f"err = {err:0.16f}") max_rel_err = max(max_rel_err, err) # Multiplication by permutation matrix of the left can be @@ -79,8 +79,7 @@ def test_layer1q_matrix(self): self.assertTrue( err1 < eps and err2 < eps and err3 < eps and err4 < eps, - "err1 = {:f}, err2 = {:f}, " - "err3 = {:f}, err4 = {:f}".format(err1, err2, err3, err4), + f"err1 = {err1:f}, err2 = {err2:f}, " f"err3 = {err3:f}, err4 = {err4:f}", ) max_rel_err = max(max_rel_err, err1, err2, err3, err4) @@ -128,12 +127,12 @@ def test_pmatrix_class(self): alt_ttmtt = pmat.finalize(temp_mat=tmp1) err1 = tut.relative_error(alt_ttmtt, ttmtt) - self.assertLess(err1, _eps, "relative error: {:f}".format(err1)) + self.assertLess(err1, _eps, f"relative error: {err1:f}") prod = np.complex128(np.trace(ttmtt @ t4)) alt_prod = pmat.product_q1(layer=c4, tmp1=tmp1, tmp2=tmp2) err2 = abs(alt_prod - prod) / abs(prod) - self.assertLess(err2, _eps, "relative error: {:f}".format(err2)) + self.assertLess(err2, _eps, f"relative error: {err2:f}") max_rel_err = max(max_rel_err, err1, err2) diff --git a/test/python/synthesis/aqc/fast_gradient/test_layer2q.py b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py index 9de1e13df2d..8f5655d6057 100644 --- a/test/python/synthesis/aqc/fast_gradient/test_layer2q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py @@ -65,7 +65,7 @@ def test_layer2q_matrix(self): # T == P^t @ G @ P. err = tut.relative_error(t_mat, iden[perm].T @ g_mat @ iden[perm]) - self.assertLess(err, _eps, "err = {:0.16f}".format(err)) + self.assertLess(err, _eps, f"err = {err:0.16f}") max_rel_err = max(max_rel_err, err) # Multiplication by permutation matrix of the left can be @@ -82,8 +82,8 @@ def test_layer2q_matrix(self): self.assertTrue( err1 < _eps and err2 < _eps and err3 < _eps and err4 < _eps, - "err1 = {:f}, err2 = {:f}, " - "err3 = {:f}, err4 = {:f}".format(err1, err2, err3, err4), + f"err1 = {err1:f}, err2 = {err2:f}, " + f"err3 = {err3:f}, err4 = {err4:f}", ) max_rel_err = max(max_rel_err, err1, err2, err3, err4) @@ -136,12 +136,12 @@ def test_pmatrix_class(self): alt_ttmtt = pmat.finalize(temp_mat=tmp1) err1 = tut.relative_error(alt_ttmtt, ttmtt) - self.assertLess(err1, _eps, "relative error: {:f}".format(err1)) + self.assertLess(err1, _eps, f"relative error: {err1:f}") prod = np.complex128(np.trace(ttmtt @ t4)) alt_prod = pmat.product_q2(layer=c4, tmp1=tmp1, tmp2=tmp2) err2 = abs(alt_prod - prod) / abs(prod) - self.assertLess(err2, _eps, "relative error: {:f}".format(err2)) + self.assertLess(err2, _eps, f"relative error: {err2:f}") max_rel_err = max(max_rel_err, err1, err2) diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index a879d5251f9..050df5a3fe1 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -78,17 +78,13 @@ def test_invalid_permutations(self, width): pattern_out_of_range[0] = width with self.assertRaises(ValueError) as exc: _validate_permutation(pattern_out_of_range) - self.assertIn( - "input has length {0} and contains {0}".format(width), str(exc.exception) - ) + self.assertIn(f"input has length {width} and contains {width}", str(exc.exception)) pattern_duplicate = np.copy(pattern) pattern_duplicate[-1] = pattern[0] with self.assertRaises(ValueError) as exc: _validate_permutation(pattern_duplicate) - self.assertIn( - "input contains {} more than once".format(pattern[0]), str(exc.exception) - ) + self.assertIn(f"input contains {pattern[0]} more than once", str(exc.exception)) @data(4, 5, 10, 15, 20) def test_synth_permutation_basic(self, width): diff --git a/test/python/test_user_config.py b/test/python/test_user_config.py index ecc4ffaaa96..5b63462963c 100644 --- a/test/python/test_user_config.py +++ b/test/python/test_user_config.py @@ -25,7 +25,7 @@ class TestUserConfig(QiskitTestCase): def setUp(self): super().setUp() - self.file_path = "test_%s.conf" % uuid4() + self.file_path = f"test_{uuid4()}.conf" def test_empty_file_read(self): config = user_config.UserConfig(self.file_path) diff --git a/test/python/transpiler/test_pass_scheduler.py b/test/python/transpiler/test_pass_scheduler.py index 6d6026d6148..12ba81c78a6 100644 --- a/test/python/transpiler/test_pass_scheduler.py +++ b/test/python/transpiler/test_pass_scheduler.py @@ -703,7 +703,7 @@ def assertPassLog(self, passmanager, list_of_passes): output_lines = self.output.readlines() pass_log_lines = [x for x in output_lines if x.startswith("Pass:")] for index, pass_name in enumerate(list_of_passes): - self.assertTrue(pass_log_lines[index].startswith("Pass: %s -" % pass_name)) + self.assertTrue(pass_log_lines[index].startswith(f"Pass: {pass_name} -")) def test_passes(self): """Dump passes in different FlowControllerLinear""" diff --git a/test/python/visualization/timeline/test_generators.py b/test/python/visualization/timeline/test_generators.py index 66afe3556b3..5554248089e 100644 --- a/test/python/visualization/timeline/test_generators.py +++ b/test/python/visualization/timeline/test_generators.py @@ -109,9 +109,7 @@ def test_gen_full_gate_name_with_finite_duration(self): self.assertListEqual(list(drawing_obj.yvals), [0.0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u3(0.00, 0.00, 0.00)[20]") - ref_latex = "{name}(0.00, 0.00, 0.00)[20]".format( - name=self.formatter["latex_symbol.gates"]["u3"] - ) + ref_latex = f"{self.formatter['latex_symbol.gates']['u3']}(0.00, 0.00, 0.00)[20]" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -132,7 +130,7 @@ def test_gen_full_gate_name_with_zero_duration(self): self.assertListEqual(list(drawing_obj.yvals), [self.formatter["label_offset.frame_change"]]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u1(0.00)") - ref_latex = "{name}(0.00)".format(name=self.formatter["latex_symbol.gates"]["u1"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u1']}(0.00)" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -159,7 +157,7 @@ def test_gen_short_gate_name_with_finite_duration(self): self.assertListEqual(list(drawing_obj.yvals), [0.0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u3") - ref_latex = "{name}".format(name=self.formatter["latex_symbol.gates"]["u3"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u3']}" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -180,7 +178,7 @@ def test_gen_short_gate_name_with_zero_duration(self): self.assertListEqual(list(drawing_obj.yvals), [self.formatter["label_offset.frame_change"]]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u1") - ref_latex = "{name}".format(name=self.formatter["latex_symbol.gates"]["u1"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u1']}" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -250,6 +248,7 @@ def test_gen_bit_name(self): self.assertListEqual(list(drawing_obj.yvals), [0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "bar") + # pylint: disable-next=consider-using-f-string ref_latex = r"{{\rm {register}}}_{{{index}}}".format(register="q", index="0") self.assertEqual(drawing_obj.latex, ref_latex) diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index 2dde71a5d39..04dced90dfa 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -306,10 +306,9 @@ def equivalent_transpile(self, kwargs): count_differences = dicts_almost_equal(aer_counts, xpiled_aer_counts, 0.05 * shots) - assert ( - count_differences == "" - ), "Counts not equivalent: {}\nFailing QASM Input:\n{}\n\nFailing QASM Output:\n{}".format( - count_differences, qasm2.dumps(self.qc), qasm2.dumps(xpiled_qc) + assert count_differences == "", ( + f"Counts not equivalent: {count_differences}\nFailing QASM Input:\n" + f"{qasm2.dumps(self.qc)}\n\nFailing QASM Output:\n{qasm2.dumps(xpiled_qc)}" ) diff --git a/test/utils/base.py b/test/utils/base.py index 747be7f66b5..63a8bf4384f 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -81,10 +81,10 @@ def setUp(self): self.addTypeEqualityFunc(QuantumCircuit, self.assertQuantumCircuitEqual) if self.__setup_called: raise ValueError( - "In File: %s\n" + f"In File: {(sys.modules[self.__class__.__module__].__file__,)}\n" "TestCase.setUp was already called. Do not explicitly call " "setUp from your tests. In your own setUp, use super to call " - "the base setUp." % (sys.modules[self.__class__.__module__].__file__,) + "the base setUp." ) self.__setup_called = True @@ -92,10 +92,10 @@ def tearDown(self): super().tearDown() if self.__teardown_called: raise ValueError( - "In File: %s\n" + f"In File: {(sys.modules[self.__class__.__module__].__file__,)}\n" "TestCase.tearDown was already called. Do not explicitly call " "tearDown from your tests. In your own tearDown, use super to " - "call the base tearDown." % (sys.modules[self.__class__.__module__].__file__,) + "call the base tearDown." ) self.__teardown_called = True @@ -305,10 +305,10 @@ def valid_comparison(value): if places is not None: if delta is not None: raise TypeError("specify delta or places not both") - msg_suffix = " within %s places" % places + msg_suffix = f" within {places} places" else: delta = delta or 1e-8 - msg_suffix = " within %s delta" % delta + msg_suffix = f" within {delta} delta" # Compare all keys in both dicts, populating error_msg. error_msg = "" diff --git a/test/visual/results.py b/test/visual/results.py index efaf09bbbbf..76fde794b39 100644 --- a/test/visual/results.py +++ b/test/visual/results.py @@ -83,30 +83,30 @@ def _new_gray(size, color): @staticmethod def passed_result_html(result, reference, diff, title): """Creates the html for passing tests""" - ret = '
%s ' % title + ret = f'
{title} ' ret += "" - ret += '
' % result - ret += '' % reference - ret += '' % diff + ret += f'
' + ret += f'' + ret += f'' ret += "
" return ret @staticmethod def failed_result_html(result, reference, diff, title): """Creates the html for failing tests""" - ret = '
%s ' % title + ret = f'
{title} ' ret += "" - ret += '
' % result - ret += '' % reference - ret += '' % diff + ret += f'
' + ret += f'' + ret += f'' ret += "
" return ret @staticmethod def no_reference_html(result, title): """Creates the html for missing-reference tests""" - ret = '
%s ' % title - ret += '" % (name, fullpath_name, fullpath_reference) + f'Download this image' + f" to {fullpath_reference}" + " and add/push to the repo" ) ret += Results.no_reference_html(fullpath_name, title) ret += "" diff --git a/tools/build_standard_commutations.py b/tools/build_standard_commutations.py index 56c452b11ce..0e1fcdf1797 100644 --- a/tools/build_standard_commutations.py +++ b/tools/build_standard_commutations.py @@ -143,12 +143,14 @@ def _dump_commuting_dict_as_python( dir_str = "standard_gates_commutations = {\n" for k, v in commutations.items(): if not isinstance(v, dict): + # pylint: disable-next=consider-using-f-string dir_str += ' ("{}", "{}"): {},\n'.format(*k, v) else: + # pylint: disable-next=consider-using-f-string dir_str += ' ("{}", "{}"): {{\n'.format(*k) for entry_key, entry_val in v.items(): - dir_str += " {}: {},\n".format(entry_key, entry_val) + dir_str += f" {entry_key}: {entry_val},\n" dir_str += " },\n" dir_str += "}\n" diff --git a/tools/find_stray_release_notes.py b/tools/find_stray_release_notes.py index 7e04f5ecc32..d694e0d89b8 100755 --- a/tools/find_stray_release_notes.py +++ b/tools/find_stray_release_notes.py @@ -49,7 +49,7 @@ def _main(): failed_files = [x for x in res if x is not None] if len(failed_files) > 0: for failed_file in failed_files: - sys.stderr.write("%s is not in the correct location.\n" % failed_file) + sys.stderr.write(f"{failed_file} is not in the correct location.\n") sys.exit(1) sys.exit(0) diff --git a/tools/verify_headers.py b/tools/verify_headers.py index 7bd7d2bad4e..552372b7725 100755 --- a/tools/verify_headers.py +++ b/tools/verify_headers.py @@ -88,18 +88,18 @@ def validate_header(file_path): break if file_path.endswith(".rs"): if "".join(lines[start : start + 2]) != header_rs: - return (file_path, False, "Header up to copyright line does not match: %s" % header) + return (file_path, False, f"Header up to copyright line does not match: {header}") if not copyright_line.search(lines[start + 2]): return (file_path, False, "Header copyright line not found") if "".join(lines[start + 3 : start + 11]) != apache_text_rs: - return (file_path, False, "Header apache text string doesn't match:\n %s" % apache_text) + return (file_path, False, f"Header apache text string doesn't match:\n {apache_text}") else: if "".join(lines[start : start + 2]) != header: - return (file_path, False, "Header up to copyright line does not match: %s" % header) + return (file_path, False, f"Header up to copyright line does not match: {header}") if not copyright_line.search(lines[start + 2]): return (file_path, False, "Header copyright line not found") if "".join(lines[start + 3 : start + 11]) != apache_text: - return (file_path, False, "Header apache text string doesn't match:\n %s" % apache_text) + return (file_path, False, f"Header apache text string doesn't match:\n {apache_text}") return (file_path, True, None) @@ -122,8 +122,8 @@ def _main(): failed_files = [x for x in res if x[1] is False] if len(failed_files) > 0: for failed_file in failed_files: - sys.stderr.write("%s failed header check because:\n" % failed_file[0]) - sys.stderr.write("%s\n\n" % failed_file[2]) + sys.stderr.write(f"{failed_file[0]} failed header check because:\n") + sys.stderr.write(f"{failed_file[2]}\n\n") sys.exit(1) sys.exit(0) From 0f513577b31c984520c8598a84b39149513ed115 Mon Sep 17 00:00:00 2001 From: Luis J Camargo Date: Wed, 19 Jun 2024 09:50:03 -0600 Subject: [PATCH 125/159] Spellcheck Done [Unitary Hack 2024] (#12501) * spell check iter1 * spell check iter 2 * Fix fmt * Update qiskit/_numpy_compat.py * Update qiskit/synthesis/evolution/product_formula.py * Update qiskit/synthesis/evolution/product_formula.py * Update releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml * undo some corrections --------- Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Co-authored-by: Luciano Bello Co-authored-by: Julien Gacon --- .binder/postBuild | 2 +- .github/workflows/backport.yml | 2 +- CONTRIBUTING.md | 4 +-- crates/README.md | 8 +++--- crates/accelerate/src/pauli_exp_val.rs | 8 +++--- crates/accelerate/src/sabre/sabre_dag.rs | 2 +- crates/accelerate/src/sparse_pauli_op.rs | 8 +++--- crates/accelerate/src/two_qubit_decompose.rs | 4 +-- crates/qasm2/src/expr.rs | 6 ++-- crates/qasm2/src/lex.rs | 6 ++-- crates/qasm2/src/parse.rs | 2 +- crates/qasm3/src/build.rs | 4 +-- crates/qasm3/src/circuit.rs | 2 +- crates/qasm3/src/expr.rs | 2 +- docs/conf.py | 4 +-- qiskit/_numpy_compat.py | 2 +- qiskit/circuit/__init__.py | 2 +- qiskit/circuit/_classical_resource_map.py | 10 +++---- qiskit/circuit/classical/expr/expr.py | 8 +++--- qiskit/circuit/classical/expr/visitors.py | 2 +- qiskit/circuit/classical/types/__init__.py | 2 +- qiskit/circuit/classical/types/types.py | 4 +-- qiskit/circuit/controlflow/_builder_utils.py | 2 +- qiskit/circuit/controlflow/builder.py | 8 +++--- qiskit/circuit/controlflow/if_else.py | 4 +-- qiskit/circuit/controlflow/switch_case.py | 2 +- qiskit/circuit/instruction.py | 6 ++-- .../arithmetic/linear_amplitude_function.py | 2 +- qiskit/circuit/library/n_local/n_local.py | 4 +-- qiskit/circuit/library/standard_gates/u.py | 2 +- qiskit/circuit/library/standard_gates/x.py | 2 +- qiskit/circuit/parameter.py | 2 +- qiskit/circuit/parameterexpression.py | 6 ++-- qiskit/circuit/quantumcircuit.py | 28 +++++++++---------- qiskit/circuit/random/utils.py | 7 +++-- qiskit/circuit/singleton.py | 6 ++-- qiskit/converters/circuit_to_instruction.py | 2 +- qiskit/dagcircuit/dagcircuit.py | 4 +-- qiskit/passmanager/passmanager.py | 4 +-- qiskit/primitives/base/base_estimator.py | 2 +- .../primitives/containers/bindings_array.py | 4 +-- qiskit/providers/backend.py | 2 +- qiskit/providers/options.py | 2 +- qiskit/qasm2/__init__.py | 14 +++++----- qiskit/qasm2/export.py | 2 +- qiskit/qasm2/parse.py | 2 +- qiskit/qasm3/ast.py | 2 +- qiskit/qasm3/exporter.py | 6 ++-- qiskit/qobj/converters/pulse_instruction.py | 4 +-- qiskit/qpy/binary_io/schedules.py | 2 +- qiskit/qpy/type_keys.py | 4 +-- .../operators/channel/transformations.py | 2 +- .../operators/dihedral/dihedral.py | 4 +-- qiskit/quantum_info/operators/measures.py | 2 +- .../operators/symplectic/clifford.py | 2 +- .../operators/symplectic/pauli_list.py | 2 +- .../operators/symplectic/random.py | 2 +- qiskit/quantum_info/states/stabilizerstate.py | 10 +++---- qiskit/result/counts.py | 2 +- .../clifford/clifford_decompose_layers.py | 2 +- .../cnotdihedral_decompose_full.py | 2 +- .../cnotdihedral_decompose_general.py | 2 +- .../discrete_basis/solovay_kitaev.py | 2 +- qiskit/synthesis/linear/linear_depth_lnn.py | 2 +- .../synthesis/linear_phase/cx_cz_depth_lnn.py | 6 ++-- qiskit/synthesis/linear_phase/cz_depth_lnn.py | 2 +- .../stabilizer/stabilizer_decompose.py | 2 +- .../two_qubit/two_qubit_decompose.py | 2 +- .../synthesis/unitary/aqc/cnot_structures.py | 2 +- qiskit/synthesis/unitary/qsd.py | 2 +- .../passes/basis/basis_translator.py | 4 +-- .../transpiler/passes/layout/sabre_layout.py | 4 +-- qiskit/transpiler/passes/layout/vf2_layout.py | 2 +- .../optimization/commutative_cancellation.py | 2 +- .../template_matching/backward_match.py | 2 +- .../template_substitution.py | 2 +- .../passes/routing/star_prerouting.py | 2 +- .../passes/scheduling/alignments/__init__.py | 4 +-- .../passes/scheduling/base_scheduler.py | 2 +- .../passes/scheduling/dynamical_decoupling.py | 2 +- .../passes/scheduling/padding/base_padding.py | 2 +- .../padding/dynamical_decoupling.py | 4 +-- .../passes/synthesis/unitary_synthesis.py | 2 +- .../transpiler/passes/utils/gate_direction.py | 2 +- qiskit/transpiler/passmanager.py | 2 +- qiskit/utils/classtools.py | 6 ++-- qiskit/utils/lazy_tester.py | 4 +-- qiskit/utils/optionals.py | 16 +++++------ qiskit/visualization/circuit/_utils.py | 2 +- qiskit/visualization/pulse_v2/events.py | 2 +- releasenotes/config.yaml | 4 +-- .../0.12/operator-dot-fd90e7e5ad99ff9b.yaml | 2 +- .../0.13/0.13.0-release-a92553cf72c203aa.yaml | 2 +- ...e-job-status-methods-3ab9646c5f5470a6.yaml | 2 +- ...efault-schedule-name-51ba198cf08978cd.yaml | 2 +- .../qinfo-operators-0193871295190bad.yaml | 8 +++--- .../0.13/qinfo-states-7f67e2432cf0c12c.yaml | 2 +- ...sition-visualization-a62d0d119569fa05.yaml | 2 +- .../parameter-conjugate-a16fd7ae0dc18ede.yaml | 2 +- .../delay-in-circuit-33f0d81783ac12ea.yaml | 2 +- ...n-setting-ctrl_state-2f9af3b9f0f7903f.yaml | 2 +- .../remove-dagnode-dict-32fa35479c0a8331.yaml | 2 +- .../add-schedule-block-c37527f3205b7b62.yaml | 2 +- ...asicaer-new-provider-ea7cf756df231c2b.yaml | 2 +- .../deprecate-schemas-424c29fbd35c90de.yaml | 2 +- .../notes/0.17/ecr-gate-45cfda1b84ac792c.yaml | 2 +- ...ircular-entanglement-0acf0195138b6aa2.yaml | 2 +- ...e-time-visualization-b5404ad875cbdae4.yaml | 2 +- .../0.17/issue-5751-1b6249f6263c9c30.yaml | 2 +- ...skit-version-wrapper-90cb7fcffeaafd6a.yaml | 4 +-- ...replace-pulse-drawer-f9f667c8f71e1e02.yaml | 2 +- .../0.18/add-pauli-list-5644d695f91de808.yaml | 2 +- ...nite-job-submissions-d6f6a583535ca798.yaml | 2 +- .../gates-in-basis-pass-337f6637e61919db.yaml | 2 +- ...measure_all-add_bits-8525317935197b90.yaml | 2 +- .../notes/0.19/mpl-bump-33a1240266e66508.yaml | 2 +- ...t-mitigation-classes-2ef175e232d791ae.yaml | 4 +-- ...nual-warning-filters-028646b73bb86860.yaml | 8 +++--- ...parse-pauli-internal-8226b4f57a61b982.yaml | 2 +- .../0.19/vf2layout-4cea88087c355769.yaml | 2 +- ...erances-z2symmetries-9c444a7b1237252e.yaml | 2 +- ...ion-alignment-passes-ef0f20d4f89f95f3.yaml | 8 +++--- ...t-preset-passmanager-db46513a24e79aa9.yaml | 2 +- .../marginal-memory-29d9d6586ae78590.yaml | 2 +- .../vf2-post-layout-f0213e2c7ebb645c.yaml | 2 +- ...plementation-details-09b0ead8b42cacda.yaml | 2 +- ...-entanglement-nlocal-38581e4ffb7a7c68.yaml | 2 +- ...-flow-representation-09520e2838f0657e.yaml | 2 +- ...ate-direction-target-a9f0acd0cf30ed66.yaml | 2 +- .../0.22/primitive-run-5d1afab3655330a6.yaml | 2 +- ...lic-pulse-subclasses-77314a1654521852.yaml | 4 +-- ...steppable-optimizers-9d9b48ba78bd58bb.yaml | 2 +- ...nsored-subset-fitter-bd28e6e6ec5bdaae.yaml | 2 +- ...ation-reorganisation-9e302239705c7842.yaml | 2 +- .../fix-qpy-loose-bits-5283dc4ad3823ce3.yaml | 4 +-- .../notes/0.23/fix_8897-2a90c4b0857c19c2.yaml | 2 +- .../0.23/initial_state-8e20b04fc2ec2f4b.yaml | 2 +- ...ize-1q-decomposition-cb9bb4651607b639.yaml | 2 +- ...l-custom-definitions-a1b839de199ca048.yaml | 4 +-- .../add-hls-plugins-038388970ad43c55.yaml | 2 +- ...-new-symbolic-pulses-4dc46ecaaa1ba928.yaml | 2 +- ...eprecate-bip-mapping-f0025c4c724e1ec8.yaml | 2 +- ...rcuit-data-operation-1b8326b1b089f10c.yaml | 2 +- ...tensoredop-to-matrix-6f22644f1bdb8b41.yaml | 2 +- ...es-for-pulse-scaling-8369eb584c6d8fe1.yaml | 2 +- ...sm2-exporter-rewrite-8993dd24f930b180.yaml | 2 +- .../qasm2-parser-rust-ecf6570e2d445a94.yaml | 2 +- ...ints-list-optimizers-033d7439f86bbb71.yaml | 2 +- ...-propagate-condition-898052b53edb1f17.yaml | 2 +- ...ter-parameter-rebind-3c799e74456469d9.yaml | 2 +- ...-mcrz-relative-phase-6ea81a369f8bda38.yaml | 2 +- .../notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml | 2 +- ...latten-nlocal-family-292b23b99947f3c9.yaml | 2 +- .../normalize-stateprep-e21972dce8695509.yaml | 2 +- .../0.25/qpy-layout-927ab34f2b47f4aa.yaml | 2 +- ...en-swapper-rustworkx-9e02c0ab67a59fe8.yaml | 2 +- .../dag-appenders-check-84d4ef20c1e20fd0.yaml | 2 +- ...deprecate-duplicates-a871f83bbbe1c96f.yaml | 2 +- ...-basis-gatedirection-bdffad3b47c1c532.yaml | 2 +- ...pr-rvalue-conditions-8b5d5f7c015658c0.yaml | 2 +- .../fix-parameter-hash-d22c270090ffc80e.yaml | 4 +-- ...-unscheduled-warning-873f7a24c6b51e2c.yaml | 6 ++-- .../0.45/qasm2-new-api-4e1e4803d6a5a175.yaml | 2 +- .../0.45/singletons-83782de8bd062cbc.yaml | 2 +- .../changes-on-upgrade-6fcd573269a8ebc5.yaml | 2 +- .../new-features-0.9-159645f977a139f7.yaml | 4 +-- ...move-opflow-qi-utils-3debd943c65b17da.yaml | 2 +- ...fix-qdrift-evolution-bceb9c4f182ab0f5.yaml | 2 +- .../1.1/star-prerouting-0998b59880c20cef.yaml | 2 +- ...r_probabilities_dict-e53f524d115bbcfc.yaml | 2 +- setup.py | 2 +- test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm | 2 +- .../classical/test_expr_constructors.py | 4 +-- .../circuit/classical/test_expr_properties.py | 2 +- test/python/circuit/library/test_diagonal.py | 2 +- test/python/circuit/library/test_qft.py | 2 +- .../circuit/test_circuit_load_from_qpy.py | 4 +-- .../python/circuit/test_circuit_operations.py | 2 +- test/python/circuit/test_circuit_vars.py | 2 +- .../circuit/test_control_flow_builders.py | 8 +++--- test/python/circuit/test_controlled_gate.py | 2 +- test/python/circuit/test_instructions.py | 2 +- test/python/circuit/test_parameters.py | 6 ++-- test/python/compiler/test_assembler.py | 6 ++-- test/python/compiler/test_transpiler.py | 6 ++-- .../converters/test_circuit_to_instruction.py | 4 +-- test/python/dagcircuit/test_collect_blocks.py | 2 +- test/python/dagcircuit/test_dagcircuit.py | 10 +++---- .../containers/test_observables_array.py | 2 +- test/python/primitives/test_estimator.py | 8 +++--- .../primitives/test_statevector_estimator.py | 2 +- .../pulse/test_instruction_schedule_map.py | 4 +-- test/python/pulse/test_reference.py | 6 ++-- test/python/qasm3/test_export.py | 8 +++--- test/python/qasm3/test_import.py | 2 +- .../operators/symplectic/test_pauli_list.py | 4 +-- .../symplectic/test_sparse_pauli_op.py | 6 ++-- test/python/result/test_mitigators.py | 4 +-- test/python/result/test_result.py | 2 +- .../test_clifford_decompose_layers.py | 4 +-- test/python/synthesis/test_cx_cz_synthesis.py | 2 +- test/python/synthesis/test_cz_synthesis.py | 2 +- .../synthesis/test_stabilizer_synthesis.py | 4 +-- test/python/synthesis/test_synthesis.py | 2 +- test/python/test_util.py | 4 +-- .../test_instruction_alignments.py | 2 +- .../python/transpiler/test_clifford_passes.py | 2 +- .../test_commutative_cancellation.py | 10 +++---- .../transpiler/test_consolidate_blocks.py | 2 +- .../test_full_ancilla_allocation.py | 2 +- test/python/transpiler/test_gate_direction.py | 4 +-- .../transpiler/test_high_level_synthesis.py | 4 +-- .../transpiler/test_instruction_alignments.py | 2 +- .../transpiler/test_preset_passmanagers.py | 4 +-- test/python/transpiler/test_sabre_layout.py | 2 +- test/python/transpiler/test_sabre_swap.py | 2 +- .../transpiler/test_template_matching.py | 4 +-- test/python/transpiler/test_token_swapper.py | 2 +- .../test_unitary_synthesis_plugin.py | 4 +-- test/python/utils/test_lazy_loaders.py | 2 +- .../visualization/test_circuit_text_drawer.py | 4 +-- test/qpy_compat/test_qpy.py | 2 +- test/utils/_canonical.py | 2 +- 223 files changed, 370 insertions(+), 369 deletions(-) diff --git a/.binder/postBuild b/.binder/postBuild index 9517953258f..cb06527f280 100644 --- a/.binder/postBuild +++ b/.binder/postBuild @@ -7,7 +7,7 @@ # - pylatexenc: for MPL drawer # - pillow: for image comparison # - appmode: jupyter extension for executing the notebook -# - seaborn: visualisation pacakge required for some graphs +# - seaborn: visualization pacakge required for some graphs pip install matplotlib pylatexenc pillow appmode seaborn pip install . diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index bcc86d63fcf..88fd919e8ad 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,6 +1,6 @@ name: Backport metadata -# Mergify manages the opening of the backport PR, this workflow is just to extend its behaviour to +# Mergify manages the opening of the backport PR, this workflow is just to extend its behavior to # do useful things like copying across the tagged labels and milestone from the base PR. on: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4641c7878fc..7076c1571b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -532,7 +532,7 @@ we used in our CI systems more closely. ### Snapshot Testing for Visualizations -If you are working on code that makes changes to any matplotlib visualisations +If you are working on code that makes changes to any matplotlib visualizations you will need to check that your changes don't break any snapshot tests, and add new tests where necessary. You can do this as follows: @@ -543,7 +543,7 @@ the snapshot tests (note this may take some time to finish loading). 3. Each test result provides a set of 3 images (left: reference image, middle: your test result, right: differences). In the list of tests the passed tests are collapsed and failed tests are expanded. If a test fails, you will see a situation like this: Screenshot_2021-03-26_at_14 13 54 -4. Fix any broken tests. Working on code for one aspect of the visualisations +4. Fix any broken tests. Working on code for one aspect of the visualizations can sometimes result in minor changes elsewhere to spacing etc. In these cases you just need to update the reference images as follows: - download the mismatched images (link at top of Jupyter Notebook output) diff --git a/crates/README.md b/crates/README.md index cbe58afa07d..d72247bc61d 100644 --- a/crates/README.md +++ b/crates/README.md @@ -29,11 +29,11 @@ This would be a particular problem for defining the circuit object and using it ## Developer notes -### Beware of initialisation order +### Beware of initialization order -The Qiskit C extension `qiskit._accelerate` needs to be initialised in a single go. -It is the lowest part of the Python package stack, so it cannot rely on importing other parts of the Python library at initialisation time (except for exceptions through PyO3's `import_exception!` mechanism). -This is because, unlike pure-Python modules, the initialisation of `_accelerate` cannot be done partially, and many components of Qiskit import their accelerators from `_accelerate`. +The Qiskit C extension `qiskit._accelerate` needs to be initialized in a single go. +It is the lowest part of the Python package stack, so it cannot rely on importing other parts of the Python library at initialization time (except for exceptions through PyO3's `import_exception!` mechanism). +This is because, unlike pure-Python modules, the initialization of `_accelerate` cannot be done partially, and many components of Qiskit import their accelerators from `_accelerate`. In general, this should not be too onerous a requirement, but if you violate it, you might see Rust panics on import, and PyO3 should wrap that up into an exception. You might be able to track down the Rust source of the import cycle by running the import with the environment variable `RUST_BACKTRACE=full`. diff --git a/crates/accelerate/src/pauli_exp_val.rs b/crates/accelerate/src/pauli_exp_val.rs index 29f741f6cf4..52a2fc07f81 100644 --- a/crates/accelerate/src/pauli_exp_val.rs +++ b/crates/accelerate/src/pauli_exp_val.rs @@ -32,7 +32,7 @@ pub fn fast_sum_with_simd(simd: S, values: &[f64]) -> f64 { sum + tail.iter().sum::() } -/// Compute the pauli expectatation value of a statevector without x +/// Compute the pauli expectation value of a statevector without x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, /)")] pub fn expval_pauli_no_x( @@ -63,7 +63,7 @@ pub fn expval_pauli_no_x( } } -/// Compute the pauli expectatation value of a statevector with x +/// Compute the pauli expectation value of a statevector with x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, x_mask, phase, x_max, /)")] pub fn expval_pauli_with_x( @@ -121,7 +121,7 @@ pub fn expval_pauli_with_x( } } -/// Compute the pauli expectatation value of a density matrix without x +/// Compute the pauli expectation value of a density matrix without x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, /)")] pub fn density_expval_pauli_no_x( @@ -153,7 +153,7 @@ pub fn density_expval_pauli_no_x( } } -/// Compute the pauli expectatation value of a density matrix with x +/// Compute the pauli expectation value of a density matrix with x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, x_mask, phase, x_max, /)")] pub fn density_expval_pauli_with_x( diff --git a/crates/accelerate/src/sabre/sabre_dag.rs b/crates/accelerate/src/sabre/sabre_dag.rs index aa35a5d7942..d783f384462 100644 --- a/crates/accelerate/src/sabre/sabre_dag.rs +++ b/crates/accelerate/src/sabre/sabre_dag.rs @@ -27,7 +27,7 @@ pub struct DAGNode { } /// A DAG representation of the logical circuit to be routed. This represents the same dataflow -/// dependences as the Python-space [DAGCircuit], but without any information about _what_ the +/// dependencies as the Python-space [DAGCircuit], but without any information about _what_ the /// operations being performed are. Note that all the qubit references here are to "virtual" /// qubits, that is, the qubits are those specified by the user. This DAG does not need to be /// full-width on the hardware. diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 808269d8ab9..e0c80f71616 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -421,7 +421,7 @@ fn decompose_dense_inner( ) { if num_qubits == 0 { // It would be safe to `return` here, but if it's unreachable then LLVM is allowed to - // optimise out this branch entirely in release mode, which is good for a ~2% speedup. + // optimize out this branch entirely in release mode, which is good for a ~2% speedup. unreachable!("should not call this with an empty operator") } // Base recursion case. @@ -529,7 +529,7 @@ fn to_matrix_dense_inner(paulis: &MatrixCompressedPaulis, parallel: bool) -> Vec out }; let write_row = |(i_row, row): (usize, &mut [Complex64])| { - // Doing the initialisation here means that when we're in parallel contexts, we do the + // Doing the initialization here means that when we're in parallel contexts, we do the // zeroing across the whole threadpool. This also seems to give a speed-up in serial // contexts, but I don't understand that. ---Jake row.fill(Complex64::new(0.0, 0.0)); @@ -721,7 +721,7 @@ macro_rules! impl_to_matrix_sparse { // The parallel overhead from splitting a subtask is fairly high (allocating and // potentially growing a couple of vecs), so we're trading off some of Rayon's ability - // to keep threads busy by subdivision with minimising overhead; we're setting the + // to keep threads busy by subdivision with minimizing overhead; we're setting the // chunk size such that the iterator will have as many elements as there are threads. let num_threads = rayon::current_num_threads(); let chunk_size = (side + num_threads - 1) / num_threads; @@ -738,7 +738,7 @@ macro_rules! impl_to_matrix_sparse { // Since we compressed the Paulis by summing equal elements, we're // lower-bounded on the number of elements per row by this value, up to // cancellations. This should be a reasonable trade-off between sometimes - // expandin the vector and overallocation. + // expanding the vector and overallocation. let mut values = Vec::::with_capacity(chunk_size * (num_ops + 1) / 2); let mut indices = Vec::<$int_ty>::with_capacity(chunk_size * (num_ops + 1) / 2); diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index f93eb2a8d99..e8c572b0403 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -293,7 +293,7 @@ fn __num_basis_gates(basis_b: f64, basis_fidelity: f64, unitary: MatRef) -> c64::new(4.0 * c.cos(), 0.0), c64::new(4.0, 0.0), ]; - // The originial Python had `np.argmax`, which returns the lowest index in case two or more + // The original Python had `np.argmax`, which returns the lowest index in case two or more // values have a common maximum value. // `max_by` and `min_by` return the highest and lowest indices respectively, in case of ties. // So to reproduce `np.argmax`, we use `min_by` and switch the order of the @@ -587,7 +587,7 @@ impl TwoQubitWeylDecomposition { // M2 is a symmetric complex matrix. We need to decompose it as M2 = P D P^T where // P ∈ SO(4), D is diagonal with unit-magnitude elements. // - // We can't use raw `eig` directly because it isn't guaranteed to give us real or othogonal + // We can't use raw `eig` directly because it isn't guaranteed to give us real or orthogonal // eigenvectors. Instead, since `M2` is complex-symmetric, // M2 = A + iB // for real-symmetric `A` and `B`, and as diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index d8a08080a95..fe78b290e0f 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -104,7 +104,7 @@ impl From for Op { } } -/// An atom of the operator-precendence expression parsing. This is a stripped-down version of the +/// An atom of the operator-precedence expression parsing. This is a stripped-down version of the /// [Token] and [TokenType] used in the main parser. We can use a data enum here because we do not /// need all the expressive flexibility in expecting and accepting many different token types as /// we do in the main parser; it does not significantly harm legibility to simply do @@ -233,7 +233,7 @@ fn binary_power(op: Op) -> (u8, u8) { /// A subparser used to do the operator-precedence part of the parsing for individual parameter /// expressions. The main parser creates a new instance of this struct for each expression it /// expects, and the instance lives only as long as is required to parse that expression, because -/// it takes temporary resposibility for the [TokenStream] that backs the main parser. +/// it takes temporary responsibility for the [TokenStream] that backs the main parser. pub struct ExprParser<'a> { pub tokens: &'a mut Vec, pub context: &'a mut TokenContext, @@ -504,7 +504,7 @@ impl<'a> ExprParser<'a> { // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0 // integers can be interpreted as floats, and doing that allows us to gracefully handle // cases where a huge float would overflow a `usize`. Never mind that in such a case, - // there's almost certainly precision loss from the floating-point representating + // there's almost certainly precision loss from the floating-point representing // having insufficient mantissa digits to faithfully represent the angle mod 2pi; // that's not our fault in the parser. TokenType::Real | TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index f9f674cbc93..551fd2b7af4 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -21,7 +21,7 @@ //! keyword; the spec technically says that any real number is valid, but in reality that leads to //! weirdness like `200.0e-2` being a valid version specifier. We do things with a custom //! context-dependent match after seeing an `OPENQASM` token, to avoid clashes with the general -//! real-number tokenisation. +//! real-number tokenization. use hashbrown::HashMap; use pyo3::prelude::PyResult; @@ -30,7 +30,7 @@ use std::path::Path; use crate::error::{message_generic, Position, QASM2ParseError}; -/// Tokenised version information data. This is more structured than the real number suggested by +/// Tokenized version information data. This is more structured than the real number suggested by /// the specification. #[derive(Clone, Debug)] pub struct Version { @@ -353,7 +353,7 @@ impl TokenStream { line_buffer: Vec::with_capacity(80), done: false, // The first line is numbered "1", and the first column is "0". The counts are - // initialised like this so the first call to `next_byte` can easily detect that it + // initialized like this so the first call to `next_byte` can easily detect that it // needs to extract the next line. line: 0, col: 0, diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index e4c74984112..f7eceb6aeef 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -1630,7 +1630,7 @@ impl State { /// Update the parser state with the definition of a particular gate. This does not emit any /// bytecode because not all gate definitions need something passing to Python. For example, - /// the Python parser initialises its state including the built-in gates `U` and `CX`, and + /// the Python parser initializes its state including the built-in gates `U` and `CX`, and /// handles the `qelib1.inc` include specially as well. fn define_gate( &mut self, diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 2f817187625..f5cf2fd4efc 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -69,7 +69,7 @@ impl BuilderState { Err(QASM3ImporterError::new_err("cannot handle consts")) } else if decl.initializer().is_some() { Err(QASM3ImporterError::new_err( - "cannot handle initialised bits", + "cannot handle initialized bits", )) } else { self.add_clbit(py, name_id.clone()) @@ -80,7 +80,7 @@ impl BuilderState { Err(QASM3ImporterError::new_err("cannot handle consts")) } else if decl.initializer().is_some() { Err(QASM3ImporterError::new_err( - "cannot handle initialised registers", + "cannot handle initialized registers", )) } else { match dims { diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 330805fa2f8..fdd92c43c0b 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -281,7 +281,7 @@ impl PyCircuitModule { /// Circuit construction context object to provide an easier Rust-space interface for us to /// construct the Python :class:`.QuantumCircuit`. The idea of doing this from Rust space like /// this is that we might steadily be able to move more and more of it into being native Rust as -/// the Rust-space APIs around the internal circuit data stabilise. +/// the Rust-space APIs around the internal circuit data stabilize. pub struct PyCircuit(Py); impl PyCircuit { diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index e912aecdb87..64afe58991c 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -71,7 +71,7 @@ fn eval_const_int(_py: Python, _ast_symbols: &SymbolTable, expr: &asg::TExpr) -> match expr.expression() { asg::Expr::Literal(asg::Literal::Int(lit)) => Ok(*lit.value() as isize), expr => Err(QASM3ImporterError::new_err(format!( - "unhandled expression type for constant-integer evaluatation: {:?}", + "unhandled expression type for constant-integer evaluation: {:?}", expr ))), } diff --git a/docs/conf.py b/docs/conf.py index b35f5ca64d6..f6bf2faa9a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,9 +114,9 @@ autosummary_generate = True autosummary_generate_overwrite = False -# The pulse library contains some names that differ only in capitalisation, during the changeover +# The pulse library contains some names that differ only in capitalization, during the changeover # surrounding SymbolPulse. Since these resolve to autosummary filenames that also differ only in -# capitalisation, this causes problems when the documentation is built on an OS/filesystem that is +# capitalization, this causes problems when the documentation is built on an OS/filesystem that is # enforcing case-insensitive semantics. This setting defines some custom names to prevent the clash # from happening. autosummary_filename_map = { diff --git a/qiskit/_numpy_compat.py b/qiskit/_numpy_compat.py index a6c06671c98..9b6b466fbc9 100644 --- a/qiskit/_numpy_compat.py +++ b/qiskit/_numpy_compat.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Compatiblity helpers for the Numpy 1.x to 2.0 transition.""" +"""Compatibility helpers for the Numpy 1.x to 2.0 transition.""" import re import typing diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 43087760153..65a88519a0d 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -270,7 +270,7 @@ * :class:`ContinueLoopOp`, to move immediately to the next iteration of the containing loop * :class:`ForLoopOp`, to loop over a fixed range of values * :class:`IfElseOp`, to conditionally enter one of two subcircuits - * :class:`SwitchCaseOp`, to conditionally enter one of many subcicuits + * :class:`SwitchCaseOp`, to conditionally enter one of many subcircuits * :class:`WhileLoopOp`, to repeat a subcircuit until a condition is falsified. :ref:`Circuits can include classical expressions that are evaluated in real time diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py index bff7d9f80fe..ba42f15cddc 100644 --- a/qiskit/circuit/_classical_resource_map.py +++ b/qiskit/circuit/_classical_resource_map.py @@ -31,7 +31,7 @@ class VariableMapper(expr.ExprVisitor[expr.Expr]): call its :meth:`map_condition`, :meth:`map_target` or :meth:`map_expr` methods as appropriate, which will return the new object that should be used. - If an ``add_register`` callable is given to the initialiser, the mapper will use it to attempt + If an ``add_register`` callable is given to the initializer, the mapper will use it to attempt to add new aliasing registers to the outer circuit object, if there is not already a suitable register for the mapping available in the circuit. If this parameter is not given, a ``ValueError`` will be raised instead. The given ``add_register`` callable may choose to raise @@ -73,12 +73,12 @@ def _map_register(self, theirs: ClassicalRegister) -> ClassicalRegister: def map_condition(self, condition, /, *, allow_reorder=False): """Map the given ``condition`` so that it only references variables in the destination - circuit (as given to this class on initialisation). + circuit (as given to this class on initialization). If ``allow_reorder`` is ``True``, then when a legacy condition (the two-tuple form) is made on a register that has a counterpart in the destination with all the same (mapped) bits but in a different order, then that register will be used and the value suitably modified to - make the equality condition work. This is maintaining legacy (tested) behaviour of + make the equality condition work. This is maintaining legacy (tested) behavior of :meth:`.DAGCircuit.compose`; nowhere else does this, and in general this would require *far* more complex classical rewriting than Terra needs to worry about in the full expression era. """ @@ -91,7 +91,7 @@ def map_condition(self, condition, /, *, allow_reorder=False): return (self.bit_map[target], value) if not allow_reorder: return (self._map_register(target), value) - # This is maintaining the legacy behaviour of `DAGCircuit.compose`. We don't attempt to + # This is maintaining the legacy behavior of `DAGCircuit.compose`. We don't attempt to # speed-up this lookup with a cache, since that would just make the more standard cases more # annoying to deal with. mapped_bits_order = [self.bit_map[bit] for bit in target] @@ -114,7 +114,7 @@ def map_condition(self, condition, /, *, allow_reorder=False): def map_target(self, target, /): """Map the real-time variables in a ``target`` of a :class:`.SwitchCaseOp` to the new - circuit, as defined in the ``circuit`` argument of the initialiser of this class.""" + circuit, as defined in the ``circuit`` argument of the initializer of this class.""" if isinstance(target, Clbit): return self.bit_map[target] if isinstance(target, ClassicalRegister): diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 62b6829ce4a..586b06ec9db 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -53,7 +53,7 @@ class Expr(abc.ABC): expressions, and it does not make sense to add more outside of Qiskit library code. All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and - should not call the parent initialiser.""" + should not call the parent initializer.""" __slots__ = ("type",) @@ -193,7 +193,7 @@ def __copy__(self): return self def __deepcopy__(self, memo): - # ... as are all my consituent parts. + # ... as are all my constituent parts. return self @@ -241,7 +241,7 @@ class Op(enum.Enum): # If adding opcodes, remember to add helper constructor functions in `constructors.py`. # The opcode integers should be considered a public interface; they are used by - # serialisation formats that may transfer data between different versions of Qiskit. + # serialization formats that may transfer data between different versions of Qiskit. BIT_NOT = 1 """Bitwise negation. ``~operand``.""" LOGIC_NOT = 2 @@ -309,7 +309,7 @@ class Op(enum.Enum): # If adding opcodes, remember to add helper constructor functions in `constructors.py` # The opcode integers should be considered a public interface; they are used by - # serialisation formats that may transfer data between different versions of Qiskit. + # serialization formats that may transfer data between different versions of Qiskit. BIT_AND = 1 """Bitwise "and". ``lhs & rhs``.""" BIT_OR = 2 diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 744257714b7..be7e9311c37 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -29,7 +29,7 @@ class ExprVisitor(typing.Generic[_T_co]): """Base class for visitors to the :class:`Expr` tree. Subclasses should override whichever of - the ``visit_*`` methods that they are able to handle, and should be organised such that + the ``visit_*`` methods that they are able to handle, and should be organized such that non-existent methods will never be called.""" # The method names are self-explanatory and docstrings would just be noise. diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 14365fd32a6..ae38a0d97fb 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -40,7 +40,7 @@ .. autoclass:: Bool .. autoclass:: Uint -Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single +Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 04266aefd41..d20e7b5fd74 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -29,7 +29,7 @@ class _Singleton(type): - """Metaclass to make the child, which should take zero initialisation arguments, a singleton + """Metaclass to make the child, which should take zero initialization arguments, a singleton object.""" def _get_singleton_instance(cls): @@ -76,7 +76,7 @@ def __deepcopy__(self, _memo): def __setstate__(self, state): _dict, slots = state for slot, value in slots.items(): - # We need to overcome the type's enforcement of immutability post initialisation. + # We need to overcome the type's enforcement of immutability post initialization. super().__setattr__(slot, value) diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index bfb0d905387..e80910aac3e 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -127,7 +127,7 @@ def unify_circuit_resources(circuits: Iterable[QuantumCircuit]) -> Iterable[Quan This function will preferentially try to mutate its inputs if they share an ordering, but if not, it will rebuild two new circuits. This is to avoid coupling too tightly to the inner class; there is no real support for deleting or re-ordering bits within a :obj:`.QuantumCircuit` - context, and we don't want to rely on the *current* behaviour of the private APIs, since they + context, and we don't want to rely on the *current* behavior of the private APIs, since they are very liable to change. No matter the method used, circuits with unified bits and registers are returned. """ diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index bb0a30ea6af..ab464a50ca6 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -13,7 +13,7 @@ """Builder types for the basic control-flow constructs.""" # This file is in circuit.controlflow rather than the root of circuit because the constructs here -# are only intended to be localised to constructing the control flow instructions. We anticipate +# are only intended to be localized to constructing the control flow instructions. We anticipate # having a far more complete builder of all circuits, with more classical control and creation, in # the future. @@ -206,7 +206,7 @@ class InstructionPlaceholder(Instruction, abc.ABC): When appending a placeholder instruction into a circuit scope, you should create the placeholder, and then ask it what resources it should be considered as using from the start by calling :meth:`.InstructionPlaceholder.placeholder_instructions`. This set will be a subset of - the final resources it asks for, but it is used for initialising resources that *must* be + the final resources it asks for, but it is used for initializing resources that *must* be supplied, such as the bits used in the conditions of placeholder ``if`` statements. .. warning:: @@ -360,7 +360,7 @@ def __init__( which use a classical register as their condition. allow_jumps: Whether this builder scope should allow ``break`` and ``continue`` statements within it. This is intended to help give sensible error messages when - dangerous behaviour is encountered, such as using ``break`` inside an ``if`` context + dangerous behavior is encountered, such as using ``break`` inside an ``if`` context manager that is not within a ``for`` manager. This can only be safe if the user is going to place the resulting :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that uses *exactly* the same set of resources. We cannot verify this from within the @@ -395,7 +395,7 @@ def clbits(self): def allow_jumps(self): """Whether this builder scope should allow ``break`` and ``continue`` statements within it. - This is intended to help give sensible error messages when dangerous behaviour is + This is intended to help give sensible error messages when dangerous behavior is encountered, such as using ``break`` inside an ``if`` context manager that is not within a ``for`` manager. This can only be safe if the user is going to place the resulting :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that uses *exactly* the same set of diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 121d4c681f2..dd639c65f4b 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -199,7 +199,7 @@ def __init__( super().__init__( "if_else", len(self.__resources.qubits), len(self.__resources.clbits), [], label=label ) - # Set the condition after super().__init__() has initialised it to None. + # Set the condition after super().__init__() has initialized it to None. self.condition = validate_condition(condition) def with_false_block(self, false_block: ControlFlowBuilderBlock) -> "IfElsePlaceholder": @@ -236,7 +236,7 @@ def registers(self): def _calculate_placeholder_resources(self) -> InstructionResources: """Get the placeholder resources (see :meth:`.placeholder_resources`). - This is a separate function because we use the resources during the initialisation to + This is a separate function because we use the resources during the initialization to determine how we should set our ``num_qubits`` and ``num_clbits``, so we implement the public version as a cache access for efficiency. """ diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 446230c3c3c..6df8c4ef62a 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -94,7 +94,7 @@ def __init__( it's easier for things like `assign_parameters`, which need to touch each circuit object exactly once, to function.""" self._label_spec: List[Tuple[Union[int, Literal[CASE_DEFAULT]], ...]] = [] - """List of the normalised jump value specifiers. This is a list of tuples, where each tuple + """List of the normalized jump value specifiers. This is a list of tuples, where each tuple contains the values, and the indexing is the same as the values of `_case_map` and `_params`.""" self._params = [] diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index f53c5b9e9b3..1b5fca3f738 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -115,12 +115,12 @@ def base_class(self) -> Type[Instruction]: The "base class" of an instruction is the lowest class in its inheritance tree that the object should be considered entirely compatible with for _all_ circuit applications. This typically means that the subclass is defined purely to offer some sort of programmer - convenience over the base class, and the base class is the "true" class for a behavioural + convenience over the base class, and the base class is the "true" class for a behavioral perspective. In particular, you should *not* override :attr:`base_class` if you are defining a custom version of an instruction that will be implemented differently by - hardware, such as an alternative measurement strategy, or a version of a parametrised gate + hardware, such as an alternative measurement strategy, or a version of a parametrized gate with a particular set of parameters for the purposes of distinguishing it in a - :class:`.Target` from the full parametrised gate. + :class:`.Target` from the full parametrized gate. This is often exactly equivalent to ``type(obj)``, except in the case of singleton instances of standard-library instructions. These singleton instances are special subclasses of their diff --git a/qiskit/circuit/library/arithmetic/linear_amplitude_function.py b/qiskit/circuit/library/arithmetic/linear_amplitude_function.py index 0825f3f4e0a..a79670eef65 100644 --- a/qiskit/circuit/library/arithmetic/linear_amplitude_function.py +++ b/qiskit/circuit/library/arithmetic/linear_amplitude_function.py @@ -119,7 +119,7 @@ def __init__( self._image = image self._rescaling_factor = rescaling_factor - # do rescalings + # do rescaling a, b = domain c, d = image diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 25f6c27bbe1..2a750195dab 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -162,7 +162,7 @@ def __init__( self._bounds: list[tuple[float | None, float | None]] | None = None self._flatten = flatten - # During the build, if a subclass hasn't overridden our parametrisation methods, we can use + # During the build, if a subclass hasn't overridden our parametrization methods, we can use # a newer fast-path method to parametrise the rotation and entanglement blocks if internally # those are just simple stdlib gates that have been promoted to circuits. We don't # precalculate the fast-path layers themselves because there's far too much that can be @@ -1093,7 +1093,7 @@ def _stdlib_gate_from_simple_block(block: QuantumCircuit) -> _StdlibGateResult | return None instruction = block.data[0] # If the single instruction isn't a standard-library gate that spans the full width of the block - # in the correct order, we're not simple. If the gate isn't fully parametrised with pure, + # in the correct order, we're not simple. If the gate isn't fully parametrized with pure, # unique `Parameter` instances (expressions are too complex) that are in order, we're not # simple. if ( diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3495bc180f0..07684097f8c 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -183,7 +183,7 @@ def __setitem__(self, key, value): # Magic numbers: CUGate has 4 parameters, UGate has 3, with the last of CUGate's missing. if isinstance(key, slice): # We don't need to worry about the case of the slice being used to insert extra / remove - # elements because that would be "undefined behaviour" in a gate already, so we're + # elements because that would be "undefined behavior" in a gate already, so we're # within our rights to do anything at all. for i, base_key in enumerate(range(*key.indices(4))): if base_key < 0: diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 6e959b3e62c..cd4a6196382 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -972,7 +972,7 @@ def __init__( _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=4) - # seems like open controls not hapening? + # seems like open controls not happening? def _define(self): """ gate c3sqrtx a,b,c,d diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 825679f7d4f..c7a8228dd46 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -62,7 +62,7 @@ class Parameter(ParameterExpression): __slots__ = ("_uuid", "_hash") # This `__init__` does not call the super init, because we can't construct the - # `_parameter_symbols` dictionary we need to pass to it before we're entirely initialised + # `_parameter_symbols` dictionary we need to pass to it before we're entirely initialized # anyway, because `ParameterExpression` depends heavily on the structure of `Parameter`. def __init__( diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 7f839677b90..16b691480d2 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -48,7 +48,7 @@ def __init__(self, symbol_map: dict, expr): expr (sympy.Expr): Expression of :class:`sympy.Symbol` s. """ # NOTE: `Parameter.__init__` does not call up to this method, since this method is dependent - # on `Parameter` instances already being initialised enough to be hashable. If changing + # on `Parameter` instances already being initialized enough to be hashable. If changing # this method, check that `Parameter.__init__` and `__setstate__` are still valid. self._parameter_symbols = symbol_map self._parameter_keys = frozenset(p._hash_key() for p in self._parameter_symbols) @@ -421,8 +421,8 @@ def __float__(self): ) from None # In symengine, if an expression was complex at any time, its type is likely to have # stayed "complex" even when the imaginary part symbolically (i.e. exactly) - # cancelled out. Sympy tends to more aggressively recognise these as symbolically - # real. This second attempt at a cast is a way of unifying the behaviour to the + # cancelled out. Sympy tends to more aggressively recognize these as symbolically + # real. This second attempt at a cast is a way of unifying the behavior to the # more expected form for our users. cval = complex(self) if cval.imag == 0.0: diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 010a91e3639..a88dfd43ea4 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -111,7 +111,7 @@ # # If you're adding methods or attributes to `QuantumCircuit`, be sure to update the class docstring # to document them in a suitable place. The class is huge, so we do its documentation manually so -# it has at least some amount of organisational structure. +# it has at least some amount of organizational structure. class QuantumCircuit: @@ -369,7 +369,7 @@ class QuantumCircuit: ------------------------------- A :class:`.Bit` instance is, on its own, just a unique handle for circuits to use in their own - contexts. If you have got a :class:`.Bit` instance and a cirucit, just can find the contexts + contexts. If you have got a :class:`.Bit` instance and a circuit, just can find the contexts that the bit exists in using :meth:`find_bit`, such as its integer index in the circuit and any registers it is contained in. @@ -650,7 +650,7 @@ class QuantumCircuit: Finally, these methods apply particular generalized multiply controlled gates to the circuit, often with eager syntheses. They are listed in terms of the *base* gate they are controlling, - since their exact output is often a synthesised version of a gate. + since their exact output is often a synthesized version of a gate. =============================== ================================================= :class:`QuantumCircuit` method Base :mod:`qiskit.circuit.library` :class:`.Gate` @@ -2500,7 +2500,7 @@ def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = Fal and the only reference to the circuit the instructions are being appended to is within that same function. In particular, it is not safe to call :meth:`QuantumCircuit._append` on a circuit that is received by a function argument. - This is because :meth:`.QuantumCircuit._append` will not recognise the scoping + This is because :meth:`.QuantumCircuit._append` will not recognize the scoping constructs of the control-flow builder interface. Args: @@ -2582,7 +2582,7 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: my_param = Parameter("my_param") - # Create a parametrised circuit. + # Create a parametrized circuit. qc = QuantumCircuit(1) qc.rx(my_param, 0) @@ -2798,8 +2798,8 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # two classical registers we measured into above. qc.add_var(my_var, expr.bit_and(cr1, cr2)) """ - # Validate the initialiser first to catch cases where the variable to be declared is being - # used in the initialiser. + # Validate the initializer first to catch cases where the variable to be declared is being + # used in the initializer. circuit_scope = self._current_scope() # Convenience method to widen Python integer literals to the right width during the initial # lift, if the type is already known via the variable. @@ -2823,7 +2823,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V var = name_or_var circuit_scope.add_uninitialized_var(var) try: - # Store is responsible for ensuring the type safety of the initialisation. + # Store is responsible for ensuring the type safety of the initialization. store = Store(var, initial) except CircuitError: circuit_scope.remove_var(var) @@ -2853,7 +2853,7 @@ def add_uninitialized_var(self, var: expr.Var, /): # name, and to be a bit less ergonomic than `add_var` (i.e. not allowing the (name, type) # overload) to discourage people from using it when they should use `add_var`. # - # This function exists so that there is a method to emulate `copy_empty_like`'s behaviour of + # This function exists so that there is a method to emulate `copy_empty_like`'s behavior of # adding uninitialised variables, which there's no obvious way around. We need to be sure # that _some_ sort of handling of uninitialised variables is taken into account in our # structures, so that doesn't become a huge edge case, even though we make no assertions @@ -2887,7 +2887,7 @@ def add_capture(self, var: expr.Var): """ if self._control_flow_scopes: # Allow manual capturing. Not sure why it'd be useful, but there's a clear expected - # behaviour here. + # behavior here. self._control_flow_scopes[-1].use_var(var) return if self._vars_input: @@ -3656,7 +3656,7 @@ def copy_empty_like( if vars_mode == "alike": # Note that this causes the local variables to be uninitialised, because the stores are # not copied. This can leave the circuit in a potentially dangerous state for users if - # they don't re-add initialiser stores. + # they don't re-add initializer stores. cpy._vars_local = self._vars_local.copy() cpy._vars_input = self._vars_input.copy() cpy._vars_capture = self._vars_capture.copy() @@ -4061,7 +4061,7 @@ def global_phase(self, angle: ParameterValueType): 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 initialised. + # 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): @@ -4273,7 +4273,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc target._increment_instances() target._name_update() - # Normalise the inputs into simple abstract interfaces, so we've dispatched the "iteration" + # 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 @@ -5909,7 +5909,7 @@ def _pop_scope(self) -> ControlFlowBuilderBlock: """Finish a scope used in the control-flow builder interface, and return it to the caller. This should only be done by the control-flow context managers, since they naturally - synchronise the creation and deletion of stack elements.""" + synchronize the creation and deletion of stack elements.""" return self._control_flow_scopes.pop() def _peek_previous_instruction_in_scope(self) -> CircuitInstruction: diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index f27cbfbfca8..3bcdbeaef4a 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -193,7 +193,8 @@ def random_circuit( # Apply arbitrary random operations in layers across all qubits. for layer_number in range(depth): # We generate all the randomness for the layer in one go, to avoid many separate calls to - # the randomisation routines, which can be fairly slow. + # the randomization routines, which can be fairly slow. + # This reliably draws too much randomness, but it's less expensive than looping over more # calls to the rng. After, trim it down by finding the point when we've used all the qubits. @@ -239,9 +240,9 @@ def random_circuit( if not gate_added_flag: break - # For efficiency in the Python loop, this uses Numpy vectorisation to pre-calculate the + # For efficiency in the Python loop, this uses Numpy vectorization to pre-calculate the # indices into the lists of qubits and parameters for every gate, and then suitably - # randomises those lists. + # randomizes those lists. q_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) p_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) q_indices[0] = p_indices[0] = 0 diff --git a/qiskit/circuit/singleton.py b/qiskit/circuit/singleton.py index bd689b6be10..874b979ff58 100644 --- a/qiskit/circuit/singleton.py +++ b/qiskit/circuit/singleton.py @@ -42,7 +42,7 @@ heart of Qiskit's data model for circuits. From a library-author perspective, the minimum that is needed to enhance a :class:`.Gate` or -:class:`~.circuit.Instruction` with this behaviour is to inherit from :class:`SingletonGate` +:class:`~.circuit.Instruction` with this behavior is to inherit from :class:`SingletonGate` (:class:`SingletonInstruction`) instead of :class:`.Gate` (:class:`~.circuit.Instruction`), and for the ``__init__`` method to have defaults for all of its arguments (these will be the state of the singleton instance). For example:: @@ -175,7 +175,7 @@ def _singleton_lookup_key(n=1, label=None): This section is primarily developer documentation for the code; none of the machinery described here is public, and it is not safe to inherit from any of it directly. -There are several moving parts to tackle here. The behaviour of having ``XGate()`` return some +There are several moving parts to tackle here. The behavior of having ``XGate()`` return some singleton object that is an (inexact) instance of :class:`.XGate` but *without* calling ``__init__`` requires us to override :class:`type.__call__ `. This means that :class:`.XGate` must have a metaclass that defines ``__call__`` to return the singleton instance. @@ -484,7 +484,7 @@ class they are providing overrides for has more lazy attributes or user-exposed instruction._define() # We use this `list` subclass that rejects all mutation rather than a simple `tuple` because # the `params` typing is specified as `list`. Various places in the library and beyond do - # `x.params.copy()` when they want to produce a version they own, which is good behaviour, + # `x.params.copy()` when they want to produce a version they own, which is good behavior, # and would fail if we switched to a `tuple`, which has no `copy` method. instruction._params = _frozenlist(instruction._params) return instruction diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 571e330eb0d..4d0570542b0 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -62,7 +62,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None if circuit.num_input_vars: # This could be supported by moving the `input` variables to be parameters of the - # instruction, but we don't really have a good reprssentation of that yet, so safer to + # instruction, but we don't really have a good representation of that yet, so safer to # forbid it. raise QiskitError("Circuits with 'input' variables cannot yet be converted to instructions") if circuit.num_captured_vars: diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 686951f26fc..d14340a8cb9 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1525,7 +1525,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit )[0] self._multi_graph.add_edge(pred._node_id, succ._node_id, contracted_var) - # Exlude any nodes from in_dag that are not a DAGOpNode or are on + # Exclude any nodes from in_dag that are not a DAGOpNode or are on # wires outside the set specified by the wires kwarg def filter_fn(node): if not isinstance(node, DAGOpNode): @@ -1615,7 +1615,7 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ be used. propagate_condition (bool): Optional, default True. If True, a condition on the ``node`` to be replaced will be applied to the new ``op``. This is the legacy - behaviour. If either node is a control-flow operation, this will be ignored. If + behavior. If either node is a control-flow operation, this will be ignored. If the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. Returns: diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py index 99527bf584e..85f422f181b 100644 --- a/qiskit/passmanager/passmanager.py +++ b/qiskit/passmanager/passmanager.py @@ -225,7 +225,7 @@ def callback_func(**kwargs): in_programs = [in_programs] is_list = False - # If we're not going to run in parallel, we want to avoid spending time `dill` serialising + # If we're not going to run in parallel, we want to avoid spending time `dill` serializing # ourselves, since that can be quite expensive. if len(in_programs) == 1 or not should_run_in_parallel(num_processes): out = [ @@ -242,7 +242,7 @@ def callback_func(**kwargs): # Pass manager may contain callable and we need to serialize through dill rather than pickle. # See https://github.com/Qiskit/qiskit-terra/pull/3290 # Note that serialized object is deserialized as a different object. - # Thus, we can resue the same manager without state collision, without building it per thread. + # Thus, we can reuse the same manager without state collision, without building it per thread. return parallel_map( _run_workflow_in_new_process, values=in_programs, diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 9191b4162d9..0a7c0ec8628 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -203,7 +203,7 @@ class BaseEstimatorV2(ABC): @staticmethod def _make_data_bin(_: EstimatorPub) -> type[DataBin]: - # this method is present for backwards compat. new primitive implementatinos + # this method is present for backwards compat. new primitive implementations # should avoid it. return DataBin diff --git a/qiskit/primitives/containers/bindings_array.py b/qiskit/primitives/containers/bindings_array.py index 6ab60f4771d..89730e5ce94 100644 --- a/qiskit/primitives/containers/bindings_array.py +++ b/qiskit/primitives/containers/bindings_array.py @@ -95,7 +95,7 @@ def __init__( be inferred from the provided arrays. Ambiguity arises whenever the key of an entry of ``data`` contains only one parameter and the corresponding array's shape ends in a one. In this case, it can't be decided whether that one is an index over parameters, or whether - it should be encorporated in :attr:`~shape`. + it should be incorporated in :attr:`~shape`. Since :class:`~.Parameter` objects are only allowed to represent float values, this class casts all given values to float. If an incompatible dtype is given, such as complex @@ -131,7 +131,7 @@ class casts all given values to float. If an incompatible dtype is given, such a def __getitem__(self, args) -> BindingsArray: # because the parameters live on the last axis, we don't need to do anything special to - # accomodate them because there will always be an implicit slice(None, None, None) + # accommodate them because there will always be an implicit slice(None, None, None) # on all unspecified trailing dimensions # separately, we choose to not disallow args which touch the last dimension, even though it # would not be a particularly friendly way to chop parameters diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index ed9fd3fdbb8..931dbed479e 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -86,7 +86,7 @@ def __init__(self, configuration, provider=None, **fields): .. This next bit is necessary just because autosummary generally won't summarise private - methods; changing that behaviour would have annoying knock-on effects through all the + methods; changing that behavior would have annoying knock-on effects through all the rest of the documentation, so instead we just hard-code the automethod directive. """ self._configuration = configuration diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index fe4e7303a67..4d716fb372b 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -116,7 +116,7 @@ def __len__(self): def __setitem__(self, key, value): self.update_options(**{key: value}) - # backwards-compatibilty with Qiskit Experiments: + # backwards-compatibility with Qiskit Experiments: @property def __dict__(self): diff --git a/qiskit/qasm2/__init__.py b/qiskit/qasm2/__init__.py index 5a2f189c410..f17fe29113e 100644 --- a/qiskit/qasm2/__init__.py +++ b/qiskit/qasm2/__init__.py @@ -20,7 +20,7 @@ .. note:: - OpenQASM 2 is a simple language, and not suitable for general serialisation of Qiskit objects. + OpenQASM 2 is a simple language, and not suitable for general serialization of Qiskit objects. See :ref:`some discussion of alternatives below `, if that is what you are looking for. @@ -95,7 +95,7 @@ Exporting API ============= -Similar to other serialisation modules in Python, this module offers two public functions: +Similar to other serialization modules in Python, this module offers two public functions: :func:`dump` and :func:`dumps`, which take a :class:`.QuantumCircuit` and write out a representative OpenQASM 2 program to a file-like object or return a string, respectively. @@ -394,7 +394,7 @@ def add_one(x): :meth:`.QuantumCircuit.from_qasm_str` and :meth:`~.QuantumCircuit.from_qasm_file` used to make a few additions on top of the raw specification. Qiskit originally tried to use OpenQASM 2 as a sort of -serialisation format, and expanded its behaviour as Qiskit expanded. The new parser under all its +serialization format, and expanded its behavior as Qiskit expanded. The new parser under all its defaults implements the specification more strictly. In particular, in the legacy importers: @@ -445,11 +445,11 @@ def add_one(x): * the parsed grammar is effectively the same as :ref:`the strict mode of the new importers `. -You can emulate this behaviour in :func:`load` and :func:`loads` by setting `include_path` +You can emulate this behavior in :func:`load` and :func:`loads` by setting `include_path` appropriately (try inspecting the variable ``qiskit.__file__`` to find the installed location), and by passing a list of :class:`CustomInstruction` instances for each of the custom gates you care about. To make things easier we make three tuples available, which each contain one component of -a configuration that is equivalent to Qiskit's legacy converter behaviour. +a configuration that is equivalent to Qiskit's legacy converter behavior. .. py:data:: LEGACY_CUSTOM_INSTRUCTIONS @@ -473,7 +473,7 @@ def add_one(x): instruction, it does not matter how the gates are actually defined and used, the legacy importer will always attempt to output its custom objects for them. This can result in errors during the circuit construction, even after a successful parse. There is no way to emulate this buggy -behaviour with :mod:`qiskit.qasm2`; only an ``include "qelib1.inc";`` statement or the +behavior with :mod:`qiskit.qasm2`; only an ``include "qelib1.inc";`` statement or the `custom_instructions` argument can cause built-in Qiskit instructions to be used, and the signatures of these match each other. @@ -549,7 +549,7 @@ def add_one(x): def _normalize_path(path: Union[str, os.PathLike]) -> str: - """Normalise a given path into a path-like object that can be passed to Rust. + """Normalize a given path into a path-like object that can be passed to Rust. Ideally this would be something that we can convert to Rust's `OSString`, but in practice, Python uses `os.fsencode` to produce a `bytes` object, but this doesn't map especially well. diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 46471fa087b..3cf0d894255 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -308,7 +308,7 @@ def _define_custom_operation(operation, gates_to_define): lib.U3Gate, } - # In known-good situations we want to use a manually parametrised object as the source of the + # In known-good situations we want to use a manually parametrized object as the source of the # definition, but still continue to return the given object as the call-site object. if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 30c85843a36..a40270a99b8 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -287,7 +287,7 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): class _DefinedGate(Gate): """A gate object defined by a `gate` statement in an OpenQASM 2 program. This object lazily - binds its parameters to its definition, so it is only synthesised when required.""" + binds its parameters to its definition, so it is only synthesized when required.""" def __init__(self, name, num_qubits, params, gates, bytecode): self._gates = gates diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 300c53900d4..0bae60144af 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -317,7 +317,7 @@ def __init__(self, expression: Expression): class ClassicalDeclaration(Statement): - """Declaration of a classical type, optionally initialising it to a value.""" + """Declaration of a classical type, optionally initializing it to a value.""" def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=None): self.type = type_ diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 78b992b17cf..6d5344bcc25 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -467,7 +467,7 @@ def build_program(self): self.build_gate_definition(instruction) for instruction in gates_to_declare ] - # Early IBM runtime paramterisation uses unbound `Parameter` instances as `input` variables, + # Early IBM runtime parametrization uses unbound `Parameter` instances as `input` variables, # not the explicit realtime `Var` variables, so we need this explicit scan. self.hoist_global_parameter_declarations() # Qiskit's clbits and classical registers need to get mapped to implicit OQ3 variables, but @@ -681,7 +681,7 @@ def hoist_classical_register_declarations(self): doesn't involve the declaration of *new* bits or registers in inner scopes; only the :class:`.expr.Var` mechanism allows that. - The behaviour of this function depends on the setting ``allow_aliasing``. If this + The behavior of this function depends on the setting ``allow_aliasing``. If this is ``True``, then the output will be in the same form as the output of :meth:`.build_classical_declarations`, with the registers being aliases. If ``False``, it will instead return a :obj:`.ast.ClassicalDeclaration` for each classical register, and one @@ -942,7 +942,7 @@ def case(values, case_block): ), ] - # Handle the stabilised syntax. + # Handle the stabilized syntax. cases = [] default = None for values, block in instruction.operation.cases_specifier(): diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 11374e9aca9..8f34ee0855a 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -89,7 +89,7 @@ class InstructionToQobjConverter: The transfer layer format must be the text representation that coforms to the `OpenPulse specification`__. Extention to the OpenPulse can be achieved by subclassing this this with - extra methods corresponding to each augumented instruction. For example, + extra methods corresponding to each augmented instruction. For example, .. code-block:: python @@ -503,7 +503,7 @@ class QobjToInstructionConverter: The transfer layer format must be the text representation that coforms to the `OpenPulse specification`__. Extention to the OpenPulse can be achieved by subclassing this this with - extra methods corresponding to each augumented instruction. For example, + extra methods corresponding to each augmented instruction. For example, .. code-block:: python diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 1a3393b1e4c..eae5e6f57ad 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -522,7 +522,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symen metadata_deserializer (JSONDecoder): An optional JSONDecoder class that will be used for the ``cls`` kwarg on the internal ``json.load`` call used to deserialize the JSON payload used for - the :attr:`.ScheduleBlock.metadata` attribute for a schdule block + the :attr:`.ScheduleBlock.metadata` attribute for a schedule block in the file-like object. If this is not specified the circuit metadata will be parsed as JSON with the stdlib ``json.load()`` function using the default ``JSONDecoder`` class. diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 3ff6b4a35af..60262440d03 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -159,7 +159,7 @@ class Condition(IntEnum): """Type keys for the ``conditional_key`` field of the INSTRUCTION struct.""" # This class is deliberately raw integers and not in terms of ASCII characters for backwards - # compatiblity in the form as an old Boolean value was expanded; `NONE` and `TWO_TUPLE` must + # compatibility in the form as an old Boolean value was expanded; `NONE` and `TWO_TUPLE` must # have the enumeration values 0 and 1. NONE = 0 @@ -276,7 +276,7 @@ class ScheduleInstruction(TypeKeyBase): REFERENCE = b"y" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. - # Call instructon is not supported by QPY. + # Call instruction is not supported by QPY. # This instruction has been excluded from ScheduleBlock instructions with # qiskit-terra/#8005 and new instruction Reference will be added instead. # Call is only applied to Schedule which is not supported by QPY. diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 18987e5e943..8f429cad8ce 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -228,7 +228,7 @@ def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): # This should be a call to la.eigh, but there is an OpenBlas # threading issue that is causing segfaults. # Need schur here since la.eig does not - # guarentee orthogonality in degenerate subspaces + # guarantee orthogonality in degenerate subspaces w, v = la.schur(data, output="complex") w = w.diagonal().real # Check eigenvalues are non-negative diff --git a/qiskit/quantum_info/operators/dihedral/dihedral.py b/qiskit/quantum_info/operators/dihedral/dihedral.py index 75b455410f4..bcd9f6b094a 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral.py @@ -97,7 +97,7 @@ class CNOTDihedral(BaseOperator, AdjointMixin): with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ @@ -325,7 +325,7 @@ def to_circuit(self): with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ # pylint: disable=cyclic-import diff --git a/qiskit/quantum_info/operators/measures.py b/qiskit/quantum_info/operators/measures.py index 293c1236ed7..8b6350ab6fd 100644 --- a/qiskit/quantum_info/operators/measures.py +++ b/qiskit/quantum_info/operators/measures.py @@ -316,7 +316,7 @@ def cvx_bmat(mat_r, mat_i): iden = sparse.eye(dim_out) # Watrous uses row-vec convention for his Choi matrix while we use - # col-vec. It turns out row-vec convention is requried for CVXPY too + # col-vec. It turns out row-vec convention is required for CVXPY too # since the cvxpy.kron function must have a constant as its first argument. c_r = cvxpy.bmat([[cvxpy.kron(iden, r0_r), x_r], [x_r.T, cvxpy.kron(iden, r1_r)]]) c_i = cvxpy.bmat([[cvxpy.kron(iden, r0_i), x_i], [-x_i.T, cvxpy.kron(iden, r1_i)]]) diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index 9a5e8732ae6..435120dd531 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -185,7 +185,7 @@ def __init__(self, data, validate=True, copy=True): isinstance(data, (list, np.ndarray)) and (data_asarray := np.asarray(data, dtype=bool)).ndim == 2 ): - # This little dance is to avoid Numpy 1/2 incompatiblities between the availability + # This little dance is to avoid Numpy 1/2 incompatibilities between the availability # and meaning of the 'copy' argument in 'array' and 'asarray', when the input needs # its dtype converting. 'asarray' prefers to return 'self' if possible in both. if copy and np.may_share_memory(data, data_asarray): diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index f2e408dd9bd..2b6e5a8774c 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -646,7 +646,7 @@ def unique(self, return_index: bool = False, return_counts: bool = False) -> Pau index = index[sort_inds] unique = PauliList(BasePauli(self._z[index], self._x[index], self._phase[index])) - # Concatinate return tuples + # Concatenate return tuples ret = (unique,) if return_index: ret += (index,) diff --git a/qiskit/quantum_info/operators/symplectic/random.py b/qiskit/quantum_info/operators/symplectic/random.py index f9bd65ef918..06b23ca2980 100644 --- a/qiskit/quantum_info/operators/symplectic/random.py +++ b/qiskit/quantum_info/operators/symplectic/random.py @@ -163,7 +163,7 @@ def _sample_qmallows(n, rng=None): if rng is None: rng = np.random.default_rng() - # Hadmard layer + # Hadamard layer had = np.zeros(n, dtype=bool) # Permutation layer diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index 4ae16c32bf5..16abb67f223 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -297,7 +297,7 @@ def expectation_value(self, oper: Pauli, qargs: None | list = None) -> complex: # Otherwise pauli is (-1)^a prod_j S_j^b_j for Clifford stabilizers # If pauli anti-commutes with D_j then b_j = 1. - # Multiply pauli by stabilizers with anti-commuting destabilizers + # Multiply pauli by stabilizers with anti-commuting destabilisers pauli_z = (pauli.z).copy() # Make a copy of pauli.z for p in range(num_qubits): # Check if destabilizer anti-commutes @@ -646,7 +646,7 @@ def _rowsum_nondeterministic(clifford, accum, row): @staticmethod def _rowsum_deterministic(clifford, aux_pauli, row): - """Updating an auxilary Pauli aux_pauli in the + """Updating an auxiliary Pauli aux_pauli in the deterministic rowsum calculation. The StabilizerState itself is not updated.""" @@ -680,8 +680,8 @@ def _get_probabilities( Args: qubits (range): range of qubits outcome (list[str]): outcome being built - outcome_prob (float): probabilitiy of the outcome - probs (dict[str, float]): holds the outcomes and probabilitiy results + outcome_prob (float): probability of the outcome + probs (dict[str, float]): holds the outcomes and probability results outcome_bitstring (str): target outcome to measure which reduces measurements, None if not targeting a specific target """ @@ -694,7 +694,7 @@ def _get_probabilities( if outcome[i] == "X": # Retrieve the qubit for the current measurement qubit = qubits[(len(qubits) - i - 1)] - # Determine if the probabilitiy is deterministic + # Determine if the probability is deterministic if not any(ret.clifford.stab_x[:, qubit]): single_qubit_outcome: np.int64 = ret._measure_and_update(qubit, 0) if outcome_bitstring is None or ( diff --git a/qiskit/result/counts.py b/qiskit/result/counts.py index 8168a3d2190..b34aa2373fb 100644 --- a/qiskit/result/counts.py +++ b/qiskit/result/counts.py @@ -101,7 +101,7 @@ def __init__(self, data, time_taken=None, creg_sizes=None, memory_slots=None): else: raise TypeError( "Invalid input key type %s, must be either an int " - "key or string key with hexademical value or bit string" + "key or string key with hexadecimal value or bit string" ) header = {} self.creg_sizes = creg_sizes diff --git a/qiskit/synthesis/clifford/clifford_decompose_layers.py b/qiskit/synthesis/clifford/clifford_decompose_layers.py index 2fc9ca5bdb2..21bea89b657 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_layers.py +++ b/qiskit/synthesis/clifford/clifford_decompose_layers.py @@ -412,7 +412,7 @@ def _calc_pauli_diff(cliff, cliff_target): def synth_clifford_depth_lnn(cliff): - """Synthesis of a :class:`.Clifford` into layers for linear-nearest neighbour connectivity. + """Synthesis of a :class:`.Clifford` into layers for linear-nearest neighbor connectivity. The depth of the synthesized n-qubit circuit is bounded by :math:`7n+2`, which is not optimal. It should be replaced by a better algorithm that provides depth bounded by :math:`7n-4` [3]. diff --git a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py index 8131458b2d3..ae56f9926da 100644 --- a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py +++ b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py @@ -40,7 +40,7 @@ def synth_cnotdihedral_full(elem: CNOTDihedral) -> QuantumCircuit: with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ diff --git a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py index 83c63026a21..bedc5c735f0 100644 --- a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py +++ b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py @@ -38,7 +38,7 @@ def synth_cnotdihedral_general(elem: CNOTDihedral) -> QuantumCircuit: References: 1. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index e1db47beaef..2e5cfeafcec 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -109,7 +109,7 @@ def run( gate_matrix_su2 = GateSequence.from_matrix(z * gate_matrix) global_phase = np.arctan2(np.imag(z), np.real(z)) - # get the decompositon as GateSequence type + # get the decomposition as GateSequence type decomposition = self._recurse(gate_matrix_su2, recursion_degree, check_input=check_input) # simplify diff --git a/qiskit/synthesis/linear/linear_depth_lnn.py b/qiskit/synthesis/linear/linear_depth_lnn.py index 2d544f37ef9..2811b755fa4 100644 --- a/qiskit/synthesis/linear/linear_depth_lnn.py +++ b/qiskit/synthesis/linear/linear_depth_lnn.py @@ -210,7 +210,7 @@ def _north_west_to_identity(n, mat): def _optimize_cx_circ_depth_5n_line(mat): # Optimize CX circuit in depth bounded by 5n for LNN connectivity. # The algorithm [1] has two steps: - # a) transform the originl matrix to a north-west matrix (m2nw), + # a) transform the original matrix to a north-west matrix (m2nw), # b) transform the north-west matrix to identity (nw2id). # # A square n-by-n matrix A is called north-west if A[i][j]=0 for all i+j>=n diff --git a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py index 23f24e07eab..c0956ea3bc7 100644 --- a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py @@ -39,7 +39,7 @@ def _initialize_phase_schedule(mat_z): """ Given a CZ layer (represented as an n*n CZ matrix Mz) - Return a scheudle of phase gates implementing Mz in a SWAP-only netwrok + Return a schedule of phase gates implementing Mz in a SWAP-only netwrok (c.f. Alg 1, [2]) """ n = len(mat_z) @@ -173,7 +173,7 @@ def _apply_phase_to_nw_circuit(n, phase_schedule, seq, swap_plus): of exactly n layers of boxes, each being either a SWAP or a SWAP+. That is, each northwest diagonalization circuit can be uniquely represented by which of its n(n-1)/2 boxes are SWAP+ and which are SWAP. - Return a QuantumCircuit that computes the phase scheudle S inside CX + Return a QuantumCircuit that computes the phase schedule S inside CX """ cir = QuantumCircuit(n) @@ -217,7 +217,7 @@ def _apply_phase_to_nw_circuit(n, phase_schedule, seq, swap_plus): def synth_cx_cz_depth_line_my(mat_x: np.ndarray, mat_z: np.ndarray) -> QuantumCircuit: """ - Joint synthesis of a -CZ-CX- circuit for linear nearest neighbour (LNN) connectivity, + Joint synthesis of a -CZ-CX- circuit for linear nearest neighbor (LNN) connectivity, with 2-qubit depth at most 5n, based on Maslov and Yang. This method computes the CZ circuit inside the CX circuit via phase gate insertions. diff --git a/qiskit/synthesis/linear_phase/cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cz_depth_lnn.py index 6dc7db5d619..7a195f0caf9 100644 --- a/qiskit/synthesis/linear_phase/cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cz_depth_lnn.py @@ -119,7 +119,7 @@ def _create_patterns(n): def synth_cz_depth_line_mr(mat: np.ndarray) -> QuantumCircuit: - r"""Synthesis of a CZ circuit for linear nearest neighbour (LNN) connectivity, + r"""Synthesis of a CZ circuit for linear nearest neighbor (LNN) connectivity, based on Maslov and Roetteler. Note that this method *reverts* the order of qubits in the circuit, diff --git a/qiskit/synthesis/stabilizer/stabilizer_decompose.py b/qiskit/synthesis/stabilizer/stabilizer_decompose.py index c43747105d0..ecdc1b3257e 100644 --- a/qiskit/synthesis/stabilizer/stabilizer_decompose.py +++ b/qiskit/synthesis/stabilizer/stabilizer_decompose.py @@ -166,7 +166,7 @@ def _calc_pauli_diff_stabilizer(cliff, cliff_target): def synth_stabilizer_depth_lnn(stab: StabilizerState) -> QuantumCircuit: - """Synthesis of an n-qubit stabilizer state for linear-nearest neighbour connectivity, + """Synthesis of an n-qubit stabilizer state for linear-nearest neighbor connectivity, in 2-qubit depth :math:`2n+2` and two distinct CX layers, using :class:`.CXGate`\\ s and phase gates (:class:`.SGate`, :class:`.SdgGate` or :class:`.ZGate`). diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 26a5b52521b..3269797827e 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -782,7 +782,7 @@ def __call__(self, mat): # This weird duplicated lazy structure is for backwards compatibility; Qiskit has historically # always made ``two_qubit_cnot_decompose`` available publicly immediately on import, but it's quite -# expensive to construct, and we want to defer the obejct's creation until it's actually used. We +# expensive to construct, and we want to defer the object's creation until it's actually used. We # only need to pass through the public methods that take `self` as a parameter. Using `__getattr__` # doesn't work because it is only called if the normal resolution methods fail. Using # `__getattribute__` is too messy for a simple one-off use object. diff --git a/qiskit/synthesis/unitary/aqc/cnot_structures.py b/qiskit/synthesis/unitary/aqc/cnot_structures.py index 8659f0c2c34..978b1fc84e6 100644 --- a/qiskit/synthesis/unitary/aqc/cnot_structures.py +++ b/qiskit/synthesis/unitary/aqc/cnot_structures.py @@ -133,7 +133,7 @@ def _get_connectivity(num_qubits: int, connectivity: str) -> dict: links = {i: list(range(num_qubits)) for i in range(num_qubits)} elif connectivity == "line": - # Every qubit is connected to its immediate neighbours only. + # Every qubit is connected to its immediate neighbors only. links = {i: [i - 1, i, i + 1] for i in range(1, num_qubits - 1)} # first qubit diff --git a/qiskit/synthesis/unitary/qsd.py b/qiskit/synthesis/unitary/qsd.py index 80a8afc1311..525daa3caf1 100644 --- a/qiskit/synthesis/unitary/qsd.py +++ b/qiskit/synthesis/unitary/qsd.py @@ -269,7 +269,7 @@ def _apply_a2(circ): # rolling over diagonals ind2 = None # lint for ind1, ind2 in zip(ind2q[0:-1:], ind2q[1::]): - # get neigboring 2q gates separated by controls + # get neighboring 2q gates separated by controls instr1 = ccirc.data[ind1] mat1 = Operator(instr1.operation).data instr2 = ccirc.data[ind2] diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 936613744b8..f2e752dd94f 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -466,7 +466,7 @@ def discover_vertex(self, v, score): score, ) self._basis_transforms.append((gate.name, gate.num_qubits, rule.params, rule.circuit)) - # we can stop the search if we have found all gates in the original ciruit. + # we can stop the search if we have found all gates in the original circuit. if not self._source_gates_remain: # if we start from source gates and apply `basis_transforms` in reverse order, we'll end # up with gates in the target basis. Note though that `basis_transforms` may include @@ -548,7 +548,7 @@ def _basis_search(equiv_lib, source_basis, target_basis): if not source_basis: return [] - # This is only neccessary since gates in target basis are currently reported by + # This is only necessary since gates in target basis are currently reported by # their names and we need to have in addition the number of qubits they act on. target_basis_keys = [key for key in equiv_lib.keys() if key.name in target_basis] diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 31609b87868..2fb9a1890bd 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -144,7 +144,7 @@ def __init__( with the ``routing_pass`` argument and an error will be raised if both are used. layout_trials (int): The number of random seed trials to run - layout with. When > 1 the trial that resuls in the output with + layout with. When > 1 the trial that results in the output with the fewest swap gates will be selected. If this is not specified (and ``routing_pass`` is not set) then the number of local physical CPUs will be used as the default value. This option is @@ -420,7 +420,7 @@ def _inner_run(self, dag, coupling_map, starting_layouts=None): ) def _ancilla_allocation_no_pass_manager(self, dag): - """Run the ancilla-allocation and -enlargment passes on the DAG chained onto our + """Run the ancilla-allocation and -enlargement passes on the DAG chained onto our ``property_set``, skipping the DAG-to-circuit conversion cost of using a ``PassManager``.""" ancilla_pass = FullAncillaAllocation(self.coupling_map) ancilla_pass.property_set = self.property_set diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 626f8f2b0fa..2e799ffa4d9 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -195,7 +195,7 @@ def mapping_to_layout(layout_mapping): if len(cm_graph) == len(im_graph): chosen_layout = mapping_to_layout(layout_mapping) break - # If there is no error map avilable we can just skip the scoring stage as there + # If there is no error map available we can just skip the scoring stage as there # is nothing to score with, so any match is the best we can find. if self.avg_error_map is None: chosen_layout = mapping_to_layout(layout_mapping) diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 68d40f3650a..4c6c487a0ea 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -75,7 +75,7 @@ def run(self, dag): var_z_gate = None z_var_gates = [gate for gate in dag.count_ops().keys() if gate in self._var_z_map] if z_var_gates: - # priortize z gates in circuit + # prioritize z gates in circuit var_z_gate = self._var_z_map[next(iter(z_var_gates))] else: z_var_gates = [gate for gate in self.basis if gate in self._var_z_map] diff --git a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py index a4b11a33de2..d194d1cbbdd 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py @@ -622,7 +622,7 @@ def run_backward_match(self): ) self.matching_list.append_scenario(new_matching_scenario) - # Third option: if blocking the succesors breaks a match, we consider + # Third option: if blocking the successors breaks a match, we consider # also the possibility to block all predecessors (push the gate to the left). if broken_matches and all(global_broken): diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py index 06c5186d284..44689894176 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py +++ b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py @@ -507,7 +507,7 @@ def _attempt_bind(self, template_sublist, circuit_sublist): to_native_symbolic = lambda x: x circuit_params, template_params = [], [] - # Set of all parameter names that are present in the circuits to be optimised. + # Set of all parameter names that are present in the circuits to be optimized. circuit_params_set = set() template_dag_dep = copy.deepcopy(self.template_dag_dep) diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index b79a298ad59..3679e8bfb8e 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -260,7 +260,7 @@ def run(self, dag): # star block by a linear sequence of gates new_dag, qubit_mapping = self.star_preroute(dag, star_blocks, processing_order) - # Fix output permuation -- copied from ElidePermutations + # Fix output permutation -- copied from ElidePermutations input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} self.property_set["original_layout"] = Layout(input_qubit_mapping) if self.property_set["original_qubit_indices"] is None: diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 513144937ab..8478f241c26 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -44,7 +44,7 @@ multiple of this value. Violation of this constraint may result in the backend execution failure. - In most of the senarios, the scheduled start time of ``DAGOpNode`` corresponds to the + In most of the scenarios, the scheduled start time of ``DAGOpNode`` corresponds to the start time of the underlying pulse instruction composing the node operation. However, this assumption can be intentionally broken by defining a pulse gate, i.e. calibration, with the schedule involving pre-buffer, i.e. some random pulse delay @@ -62,7 +62,7 @@ This value is reported by ``timing_constraints["granularity"]`` in the backend configuration in units of dt. This is the constraint for a single pulse :class:`Play` instruction that may constitute your pulse gate. - The length of waveform samples should be multipel of this constraint value. + The length of waveform samples should be multiple of this constraint value. Violation of this constraint may result in failue in backend execution. Minimum pulse length constraint diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 4085844a470..e9076c5c637 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -68,7 +68,7 @@ class BaseSchedulerTransform(TransformationPass): However, such optimization should be done by another pass, otherwise scheduling may break topological ordering of the original circuit. - Realistic control flow scheduling respecting for microarcitecture + Realistic control flow scheduling respecting for microarchitecture In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) followed by resonator ring-down (depopulation). This microwave signal is recorded diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 12f4bc515b2..7be0e838ebf 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -130,7 +130,7 @@ def __init__( will be used [d/2, d, d, ..., d, d, d/2]. skip_reset_qubits (bool): if True, does not insert DD on idle periods that immediately follow initialized/reset qubits (as - qubits in the ground state are less susceptile to decoherence). + qubits in the ground state are less susceptible to decoherence). target (Target): The :class:`~.Target` representing the target backend, if both ``durations`` and this are specified then this argument will take precedence and ``durations`` will be ignored. diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index a90f0c339ce..4ce17e7bc26 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -202,7 +202,7 @@ def _apply_scheduled_op( ): """Add new operation to DAG with scheduled information. - This is identical to apply_operation_back + updating the node_start_time propety. + This is identical to apply_operation_back + updating the node_start_time property. Args: dag: DAG circuit on which the sequence is applied. diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 806e001f2bd..45333de009b 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -128,7 +128,7 @@ def __init__( will be used [d/2, d, d, ..., d, d, d/2]. skip_reset_qubits: If True, does not insert DD on idle periods that immediately follow initialized/reset qubits - (as qubits in the ground state are less susceptile to decoherence). + (as qubits in the ground state are less susceptible to decoherence). pulse_alignment: The hardware constraints for gate timing allocation. This is usually provided from ``backend.configuration().timing_constraints``. If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to @@ -311,7 +311,7 @@ def _pad( # slack = 992 dt - 4 x 160 dt = 352 dt # # unconstraind sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt - # constraind sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt + # constrained sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt # # Now we evenly split extra slack into start and end of the sequence. # The distributed slack should be multiple of 16. diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index a30411d16a9..7db48d6d139 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -414,7 +414,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.method == "default": # If the method is the default, we only need to evaluate one set of keyword arguments. # To simplify later logic, and avoid cases where static analysis might complain that we - # haven't initialised the "default" handler, we rebind the names so they point to the + # haven't initialized the "default" handler, we rebind the names so they point to the # same object as the chosen method. default_method = plugin_method default_kwargs = plugin_kwargs diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 98b471f6f7f..79493ae8ad2 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -67,7 +67,7 @@ class GateDirection(TransformationPass): └──────┘ └───┘└──────┘└───┘ This pass assumes that the positions of the qubits in the :attr:`.DAGCircuit.qubits` attribute - are the physical qubit indicies. For example if ``dag.qubits[0]`` is qubit 0 in the + are the physical qubit indices. For example if ``dag.qubits[0]`` is qubit 0 in the :class:`.CouplingMap` or :class:`.Target`. """ diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index f590a1510bd..c905d614214 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -338,7 +338,7 @@ def __init__(self, stages: Iterable[str] | None = None, **kwargs) -> None: "scheduling", ] self._validate_stages(stages) - # Set through parent class since `__setattr__` requieres `expanded_stages` to be defined + # Set through parent class since `__setattr__` requires `expanded_stages` to be defined super().__setattr__("_stages", tuple(stages)) super().__setattr__("_expanded_stages", tuple(self._generate_expanded_stages())) super().__init__() diff --git a/qiskit/utils/classtools.py b/qiskit/utils/classtools.py index 1e58b1ad2b9..7dae35d1349 100644 --- a/qiskit/utils/classtools.py +++ b/qiskit/utils/classtools.py @@ -31,7 +31,7 @@ class _lift_to_method: # pylint: disable=invalid-name returned unchanged if so, otherwise it is turned into the default implementation for functions, which makes them bindable to instances. - Python-space functions and lambdas already have this behaviour, but builtins like ``print`` + Python-space functions and lambdas already have this behavior, but builtins like ``print`` don't; using this class allows us to do:: wrap_method(MyClass, "maybe_mutates_arguments", before=print, after=print) @@ -49,7 +49,7 @@ def __new__(cls, method): def __init__(self, method): if method is self: - # Prevent double-initialisation if we are passed an instance of this object to lift. + # Prevent double-initialization if we are passed an instance of this object to lift. return self._method = method @@ -118,7 +118,7 @@ def out(*args, **kwargs): def wrap_method(cls: Type, name: str, *, before: Callable = None, after: Callable = None): - """Wrap the functionality the instance- or class method ``cls.name`` with additional behaviour + """Wrap the functionality the instance- or class method ``cls.name`` with additional behavior ``before`` and ``after``. This mutates ``cls``, replacing the attribute ``name`` with the new functionality. This is diff --git a/qiskit/utils/lazy_tester.py b/qiskit/utils/lazy_tester.py index f2c4c380315..58c5931fd5e 100644 --- a/qiskit/utils/lazy_tester.py +++ b/qiskit/utils/lazy_tester.py @@ -174,7 +174,7 @@ def require_in_instance(self, feature_or_class: str) -> Callable[[Type], Type]: def require_in_instance(self, feature_or_class): """A class decorator that requires the dependency is available when the class is - initialised. This decorator can be used even if the class does not define an ``__init__`` + initialized. This decorator can be used even if the class does not define an ``__init__`` method. Args: @@ -186,7 +186,7 @@ def require_in_instance(self, feature_or_class): Returns: Callable: a class decorator that ensures that the wrapped feature is present if the - class is initialised. + class is initialized. """ if isinstance(feature_or_class, str): feature = feature_or_class diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index f2b1e56e112..f2e6c860faa 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -79,7 +79,7 @@ * - .. py:data:: HAS_IPYTHON - If `the IPython kernel `__ is available, certain additional - visualisations and line magics are made available. + visualizations and line magics are made available. * - .. py:data:: HAS_IPYWIDGETS - Monitoring widgets for jobs running on external backends can be provided if `ipywidgets @@ -94,7 +94,7 @@ interactivity features. * - .. py:data:: HAS_MATPLOTLIB - - Qiskit provides several visualisation tools in the :mod:`.visualization` module. + - Qiskit provides several visualization tools in the :mod:`.visualization` module. Almost all of these are built using `Matplotlib `__, which must be installed in order to use them. @@ -116,7 +116,7 @@ :class:`.DAGCircuit` in certain modes. * - .. py:data:: HAS_PYDOT - - For some graph visualisations, Qiskit uses `pydot `__ as an + - For some graph visualizations, Qiskit uses `pydot `__ as an interface to GraphViz (see :data:`HAS_GRAPHVIZ`). * - .. py:data:: HAS_PYGMENTS @@ -134,7 +134,7 @@ `__. * - .. py:data:: HAS_SEABORN - - Qiskit provides several visualisation tools in the :mod:`.visualization` module. Some + - Qiskit provides several visualization tools in the :mod:`.visualization` module. Some of these are built using `Seaborn `__, which must be installed in order to use them. @@ -179,16 +179,16 @@ :widths: 25 75 * - .. py:data:: HAS_GRAPHVIZ - - For some graph visualisations, Qiskit uses the `GraphViz `__ - visualisation tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + - For some graph visualizations, Qiskit uses the `GraphViz `__ + visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). * - .. py:data:: HAS_PDFLATEX - - Visualisation tools that use LaTeX in their output, such as the circuit drawers, require + - Visualization tools that use LaTeX in their output, such as the circuit drawers, require ``pdflatex`` to be available. You will generally need to ensure that you have a working LaTeX installation available, and the ``qcircuit.tex`` package. * - .. py:data:: HAS_PDFTOCAIRO - - Visualisation tools that convert LaTeX-generated files into rasterised images use the + - Visualization tools that convert LaTeX-generated files into rasterized images use the ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools `__. diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index c14bb3d46c2..ca29794b962 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -112,7 +112,7 @@ def get_gate_ctrl_text(op, drawer, style=None, calibrations=None): gate_text = gate_text.replace("-", "\\mbox{-}") ctrl_text = f"$\\mathrm{{{ctrl_text}}}$" - # Only captitalize internally-created gate or instruction names + # Only capitalize internally-created gate or instruction names elif ( (gate_text == op.name and op_type not in (Gate, Instruction)) or (gate_text == base_name and base_type not in (Gate, Instruction)) diff --git a/qiskit/visualization/pulse_v2/events.py b/qiskit/visualization/pulse_v2/events.py index 4bb59cd8626..74da24b1db2 100644 --- a/qiskit/visualization/pulse_v2/events.py +++ b/qiskit/visualization/pulse_v2/events.py @@ -196,7 +196,7 @@ def get_waveforms(self) -> Iterator[PulseInstruction]: def get_frame_changes(self) -> Iterator[PulseInstruction]: """Return frame change type instructions with total frame change amount.""" - # TODO parse parametrised FCs correctly + # TODO parse parametrized FCs correctly sorted_frame_changes = sorted(self._frames.items(), key=lambda x: x[0]) diff --git a/releasenotes/config.yaml b/releasenotes/config.yaml index bea33ef99a1..0c621662ca8 100644 --- a/releasenotes/config.yaml +++ b/releasenotes/config.yaml @@ -87,7 +87,7 @@ template: | New features related to the qiskit.qpy module. features_quantum_info: - | - New features releated to the qiskit.quantum_info module. + New features related to the qiskit.quantum_info module. features_synthesis: - | New features related to the qiskit.synthesis module. @@ -178,7 +178,7 @@ template: | Deprecations related to the qiskit.qpy module. deprecations_quantum_info: - | - Deprecations releated to the qiskit.quantum_info module. + Deprecations related to the qiskit.quantum_info module. deprecations_synthesis: - | Deprecations related to the qiskit.synthesis module. diff --git a/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml b/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml index 9dd4a241a09..0e778de9665 100644 --- a/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml +++ b/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml @@ -22,4 +22,4 @@ upgrade: from the right hand side of the operation if the left does not have ``__mul__`` defined) implements scalar multiplication (i.e. :meth:`qiskit.quantum_info.Operator.multiply`). Previously both methods - implemented scalar multiplciation. + implemented scalar multiplication. diff --git a/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml b/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml index 30561a9aadc..71fe512aa77 100644 --- a/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml +++ b/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml @@ -8,7 +8,7 @@ prelude: | structure behind all operations to be based on `retworkx `_ for greatly improved performance. Circuit transpilation speed in the 0.13.0 release should - be significanlty faster than in previous releases. + be significantly faster than in previous releases. There has been a significant simplification to the style in which Pulse instructions are built. Now, ``Command`` s are deprecated and a unified diff --git a/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml b/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml index 59956851a81..96fb649b07d 100644 --- a/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml +++ b/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml @@ -8,4 +8,4 @@ features: * :meth:`~qiskit.providers.BaseJob.cancelled` * :meth:`~qiskit.providers.BaseJob.in_final_state` - These methods are used to check wheter a job is in a given job status. + These methods are used to check whether a job is in a given job status. diff --git a/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml b/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml index d8df9b53fdc..24b00a262e0 100644 --- a/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml +++ b/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml @@ -2,6 +2,6 @@ fixes: - | Fixes a case in :meth:`qiskit.result.Result.get_counts`, where the results - for an expirement could not be referenced if the experiment was initialized + for an experiment could not be referenced if the experiment was initialized as a Schedule without a name. Fixes `#2753 `_ diff --git a/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml b/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml index 8384df8d205..ba4f07afd89 100644 --- a/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml +++ b/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml @@ -11,7 +11,7 @@ features: the number of two-qubit gates. - | Adds :class:`qiskit.quantum_info.SparsePauliOp` operator class. This is an - efficient representaiton of an N-qubit matrix that is sparse in the Pauli + efficient representation of an N-qubit matrix that is sparse in the Pauli basis and uses a :class:`qiskit.quantum_info.PauliTable` and vector of complex coefficients for its data structure. @@ -23,7 +23,7 @@ features: Numpy arrays or :class:`~qiskit.quantum_info.Operator` objects can be converted to a :class:`~qiskit.quantum_info.SparsePauliOp` using the `:class:`~qiskit.quantum_info.SparsePauliOp.from_operator` method. - :class:`~qiskit.quantum_info.SparsePauliOp` can be convered to a sparse + :class:`~qiskit.quantum_info.SparsePauliOp` can be converted to a sparse csr_matrix or dense Numpy array using the :class:`~qiskit.quantum_info.SparsePauliOp.to_matrix` method, or to an :class:`~qiskit.quantum_info.Operator` object using the @@ -54,7 +54,7 @@ features: :meth:`~qiskit.quantum_info.PauliTable.tensor`) between each element of the first table, with each element of the second table. - * Addition of two tables acts as list concatination of the terms in each + * Addition of two tables acts as list concatenation of the terms in each table (``+``). * Pauli tables can be sorted by lexicographic (tensor product) order or @@ -148,7 +148,7 @@ upgrade: n_qubits = 10 ham = ScalarOp(2 ** n_qubits, coeff=0) - # Add 2-body nearest neighbour terms + # Add 2-body nearest neighbor terms for j in range(n_qubits - 1): ham = ham + ZZ([j, j+1]) - | diff --git a/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml b/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml index 9a8a8453083..df465a05c43 100644 --- a/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml +++ b/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml @@ -175,7 +175,7 @@ deprecations: The ``add``, ``subtract``, and ``multiply`` methods of the :class:`qiskit.quantum_info.Statevector` and :class:`qiskit.quantum_info.DensityMatrix` classes are deprecated and will - be removed in a future release. Instead you shoulde use ``+``, ``-``, ``*`` + be removed in a future release. Instead you should use ``+``, ``-``, ``*`` binary operators instead. - | Deprecates :meth:`qiskit.quantum_info.Statevector.to_counts`, diff --git a/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml b/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml index 12c69ef47c5..21e14b6ef54 100644 --- a/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml +++ b/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml @@ -6,7 +6,7 @@ features: single qubit gate transitions has been added. It takes in a single qubit circuit and returns an animation of qubit state transitions on a Bloch sphere. To use this function you must have installed - the dependencies for and configured globally a matplotlib animtion + the dependencies for and configured globally a matplotlib animation writer. You can refer to the `matplotlib documentation `_ for more details on this. However, in the default case simply ensuring diff --git a/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml b/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml index e30386b5dfd..3fbb8558afc 100644 --- a/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml +++ b/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml @@ -5,4 +5,4 @@ features: been added to the :class:`~qiskit.circuit.ParameterExpression` class. This enables calling ``numpy.conj()`` without raising an error. Since a :class:`~qiskit.circuit.ParameterExpression` object is real, it will - return itself. This behaviour is analogous to Python floats/ints. + return itself. This behavior is analogous to Python floats/ints. diff --git a/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml b/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml index a9ffc4d508d..bc60745c71d 100644 --- a/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml +++ b/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml @@ -43,7 +43,7 @@ features: of scheduled circuits. - | - A new fuction :func:`qiskit.compiler.sequence` has been also added so that + A new function :func:`qiskit.compiler.sequence` has been also added so that we can convert a scheduled circuit into a :class:`~qiskit.pulse.Schedule` to make it executable on a pulse-enabled backend. diff --git a/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml b/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml index 9b441e252c1..158550d9a09 100644 --- a/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml +++ b/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml @@ -6,4 +6,4 @@ fixes: in the creation of the matrix for the controlled unitary and again when calling the :meth:`~qiskit.circuit.ControlledGate.definition` method of the :class:`qiskit.circuit.ControlledGate` class. This would give the - appearence that setting ``ctrl_state`` had no effect. + appearance that setting ``ctrl_state`` had no effect. diff --git a/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml b/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml index a0b69d5333c..42f69635155 100644 --- a/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml +++ b/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml @@ -3,7 +3,7 @@ upgrade: - | The previously deprecated support for passing in a dictionary as the first positional argument to :class:`~qiskit.dagcircuit.DAGNode` constructor - has been removed. Using a dictonary for the first positional argument + has been removed. Using a dictionary for the first positional argument was deprecated in the 0.13.0 release. To create a :class:`~qiskit.dagcircuit.DAGNode` object now you should directly pass the attributes as kwargs on the constructor. diff --git a/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml b/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml index 0cde632a36b..fc317417747 100644 --- a/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml +++ b/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml @@ -49,7 +49,7 @@ deprecations: constructing parameterized pulse programs. - | The :attr:`~qiskit.pulse.channels.Channel.parameters` attribute for - the following clasess: + the following classes: * :py:class:`~qiskit.pulse.channels.Channel` * :py:class:`~qiskit.pulse.instructions.Instruction`. diff --git a/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml b/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml index 85b6a4cd37b..a688665aea5 100644 --- a/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml +++ b/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml @@ -34,7 +34,7 @@ upgrade: until the simulation finishes executing. If you want to restore the previous async behavior you'll need to wrap the :meth:`~qiskit.providers.basicaer.QasmSimulatorPy.run` with something that - will run in a seperate thread or process like ``futures.ThreadPoolExecutor`` + will run in a separate thread or process like ``futures.ThreadPoolExecutor`` or ``futures.ProcessPoolExecutor``. - | The ``allow_sample_measuring`` option for the diff --git a/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml b/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml index dd9b8052e5a..d627ecfe47c 100644 --- a/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml +++ b/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml @@ -11,7 +11,7 @@ deprecations: deprecation warning). The schema files have been moved to the `Qiskit/ibmq-schemas `__ repository and those should be treated as the canonical versions of the - API schemas. Moving forward only those schemas will recieve updates and + API schemas. Moving forward only those schemas will receive updates and will be used as the source of truth for the schemas. If you were relying on the schemas bundled in qiskit-terra you should update to use that repository instead. diff --git a/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml b/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml index 5ddd2771072..42aeec0b513 100644 --- a/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml +++ b/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml @@ -25,7 +25,7 @@ features: - | Two new transpiler passess, :class:`~qiskit.transpiler.GateDirection` and class:`qiskit.transpiler.CheckGateDirection`, were added to the - :mod:`qiskit.transpiler.passes` module. These new passes are inteded to + :mod:`qiskit.transpiler.passes` module. These new passes are intended to be more general replacements for :class:`~qiskit.transpiler.passes.CXDirection` and :class:`~qiskit.transpiler.passes.CheckCXDirection` (which are both now diff --git a/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml b/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml index 1dcd1982955..4adadd4a7c8 100644 --- a/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml +++ b/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml @@ -5,6 +5,6 @@ fixes: :class:`qiskit.circuit.library.NLocal` circuit class for the edge case where the circuit has the same size as the entanglement block (e.g. a two-qubit circuit and CZ entanglement gates). In this case there should only be one entanglement - gate, but there was accidentially added a second one in the inverse direction as the + gate, but there was accidentally added a second one in the inverse direction as the first. Fixed `Qiskit/qiskit-aqua#1452 `__ diff --git a/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml b/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml index e306e5a1558..824a9556308 100644 --- a/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml +++ b/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml @@ -2,7 +2,7 @@ fixes: - | Fixed an issue with the :func:`qiskit.visualization.timeline_drawer` - function where classical bits were inproperly handled. + function where classical bits were improperly handled. Fixed `#5361 `__ - | Fixed an issue in the :func:`qiskit.visualization.circuit_drawer` function diff --git a/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml b/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml index e5553896c76..d7a8d21f974 100644 --- a/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml +++ b/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml @@ -17,5 +17,5 @@ features: the :class:`~qiskit.transpiler.passes.TemplateOptimization` pass with the :py:class:`qiskit.transpiler.passes.RZXCalibrationBuilder` pass to automatically find and replace gate sequences, such as - ``CNOT - P(theta) - CNOT``, with more efficent circuits based on + ``CNOT - P(theta) - CNOT``, with more efficient circuits based on :class:`qiskit.circuit.library.RZXGate` with a calibration. diff --git a/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml b/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml index 0bc3fbf78ff..f30cea6b27f 100644 --- a/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml +++ b/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml @@ -13,7 +13,7 @@ upgrade: this change. - | The ``qiskit.execute`` module has been renamed to - :mod:`qiskit.execute_function`. This was necessary to avoid a potentical + :mod:`qiskit.execute_function`. This was necessary to avoid a potential name conflict between the :func:`~qiskit.execute_function.execute` function which is re-exported as ``qiskit.execute``. ``qiskit.execute`` the function in some situations could conflict with ``qiskit.execute`` the module which @@ -30,7 +30,7 @@ upgrade: been renamed to ``qiskit.compiler.transpiler``, ``qiskit.compiler.assembler``, ``qiskit.compiler.scheduler``, and ``qiskit.compiler.sequence`` respectively. This was necessary to avoid a - potentical name conflict between the modules and the re-exported function + potential name conflict between the modules and the re-exported function paths :func:`qiskit.compiler.transpile`, :func:`qiskit.compiler.assemble`, :func:`qiskit.compiler.schedule`, and :func:`qiskit.compiler.sequence`. In some situations this name conflict between the module path and diff --git a/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml b/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml index ef5a733d2a0..a5a75974ed1 100644 --- a/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml +++ b/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml @@ -15,7 +15,7 @@ features: * Specifying ``axis`` objects for plotting to allow further extension of generated plots, i.e., for publication manipulations. - New stylesheets can take callback functions that dynamically modify the apperance of + New stylesheets can take callback functions that dynamically modify the appearance of the output image, for example, reassembling a collection of channels, showing details of instructions, updating appearance of pulse envelopes, etc... You can create custom callback functions and feed them into a stylesheet instance to diff --git a/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml b/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml index bd614d766f5..1c70b64a91f 100644 --- a/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml +++ b/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml @@ -4,7 +4,7 @@ features: A new class, :class:`~qiskit.quantum_info.PauliList`, has been added to the :mod:`qiskit.quantum_info` module. This class is used to efficiently represent a list of :class:`~qiskit.quantum_info.Pauli` - operators. This new class inherets from the same parent class as the + operators. This new class inherits from the same parent class as the existing :class:`~qiskit.quantum_info.PauliTable` (and therefore can be mostly used interchangeably), however it differs from the :class:`~qiskit.quantum_info.PauliTable` diff --git a/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml b/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml index b4b2bbbd8b3..c37fe6bbcdc 100644 --- a/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml +++ b/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml @@ -5,7 +5,7 @@ features: to limit the number of times a job will attempt to be executed on a backend. Previously the submission and fetching of results would be attempted infinitely, even if the job was cancelled or errored on the backend. The - default is now 50, and the previous behaviour can be achieved by setting + default is now 50, and the previous behavior can be achieved by setting ``max_job_tries=-1``. Fixes `#6872 `__ and `#6821 `__. diff --git a/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml b/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml index 2da1ce1a40d..fbde9d704a1 100644 --- a/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml +++ b/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml @@ -13,7 +13,7 @@ features: from qiskit.circuit import QuantumCircuit from qiskit.transpiler.passes import GatesInBasis - # Instatiate Pass + # Instantiate Pass basis_gates = ["cx", "h"] basis_check_pass = GatesInBasis(basis_gates) # Build circuit diff --git a/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml b/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml index d5737bba8e5..bdfca2b5c57 100644 --- a/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml +++ b/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml @@ -2,7 +2,7 @@ features: - | Added a new parameter, ``add_bits``, to :meth:`.QuantumCircuit.measure_all`. - By default it is set to ``True`` to maintain the previous behaviour of adding a new :obj:`.ClassicalRegister` of the same size as the number of qubits to store the measurements. + By default it is set to ``True`` to maintain the previous behavior of adding a new :obj:`.ClassicalRegister` of the same size as the number of qubits to store the measurements. If set to ``False``, the measurements will be stored in the already existing classical bits. For example, if you created a circuit with existing classical bits like:: diff --git a/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml b/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml index 16e636ab7c9..c553ee8f0ca 100644 --- a/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml +++ b/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml @@ -10,5 +10,5 @@ upgrade: deprecated the use of APIs around 3D visualizations that were compatible with older releases and second installing older versions of Matplotlib was becoming increasingly difficult as matplotlib's upstream dependencies - have caused incompatiblities that made testing moving forward more + have caused incompatibilities that made testing moving forward more difficult. diff --git a/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml b/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml index 69d60ee1545..26058e03f3d 100644 --- a/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml +++ b/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml @@ -25,7 +25,7 @@ features: - | Added the :class:`~qiskit.result.LocalReadoutMitigator` class for performing measurement readout error mitigation of local measurement - errors. Local measuerment errors are those that are described by a + errors. Local measurement errors are those that are described by a tensor-product of single-qubit measurement errors. This class can be initialized with a list of :math:`N` single-qubit of @@ -40,7 +40,7 @@ features: performing measurement readout error mitigation of correlated measurement errors. This class can be initialized with a single :math:`2^N \times 2^N` measurement error assignment matrix that descirbes the error probabilities. - Mitigation is implemented via inversion of assigment matrix which has + Mitigation is implemented via inversion of assignment matrix which has mitigation complexity of :math:`O(4^N)` of :class:`~qiskit.result.QuasiDistribution` and expectation values. - | diff --git a/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml b/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml index 15ee5cc07ce..1ec1774e680 100644 --- a/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml +++ b/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml @@ -2,12 +2,12 @@ upgrade: - | An internal filter override that caused all Qiskit deprecation warnings to - be displayed has been removed. This means that the behaviour will now - revert to the standard Python behaviour for deprecations; you should only + be displayed has been removed. This means that the behavior will now + revert to the standard Python behavior for deprecations; you should only see a ``DeprecationWarning`` if it was triggered by code in the main script file, interpreter session or Jupyter notebook. The user will no longer be blamed with a warning if internal Qiskit functions call deprecated - behaviour. If you write libraries, you should occasionally run with the + behavior. If you write libraries, you should occasionally run with the default warning filters disabled, or have tests which always run with them disabled. See the `Python documentation on warnings`_, and in particular the `section on testing for deprecations`_ for more information on how to do this. @@ -16,7 +16,7 @@ upgrade: .. _section on testing for deprecations: https://docs.python.org/3/library/warnings.html#updating-code-for-new-versions-of-dependencies - | Certain warnings used to be only issued once, even if triggered from - multiple places. This behaviour has been removed, so it is possible that if + multiple places. This behavior has been removed, so it is possible that if you call deprecated functions, you may see more warnings than you did before. You should change any deprecated function calls to the suggested versions, because the deprecated forms will be removed in future Qiskit diff --git a/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml b/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml index 4ce25f21146..a9623206ecd 100644 --- a/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml +++ b/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml @@ -12,5 +12,5 @@ upgrade: The return type of :func:`~qiskit.quantum_info.pauli_basis` will change from :class:`~qiskit.quantum_info.PauliTable` to :class:`~qiskit.quantum_info.PauliList` in a future release of Qiskit Terra. - To immediately swap to the new behaviour, pass the keyword argument + To immediately swap to the new behavior, pass the keyword argument ``pauli_list=True``. diff --git a/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml b/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml index 07c0ce9178d..74d66c7558c 100644 --- a/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml +++ b/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml @@ -7,7 +7,7 @@ features: `__ to find a perfect layout (a layout which would not require additional routing) if one exists. The functionality exposed by this new pass is very - similar to exisiting :class:`~qiskit.transpiler.passes.CSPLayout` but + similar to existing :class:`~qiskit.transpiler.passes.CSPLayout` but :class:`~qiskit.transpiler.passes.VF2Layout` is significantly faster. .. _VF2 algorithm: https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.101.5342&rep=rep1&type=pdf diff --git a/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml b/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml index e6dc2829ca0..db9b15d8625 100644 --- a/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml +++ b/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml @@ -35,7 +35,7 @@ features: SparsePauliOp(['X', 'Y'], coeffs=[1.+0.j, 0.+1.j]) - Note that the chop method does not accumulate the coefficents of the same Paulis, e.g. + Note that the chop method does not accumulate the coefficients of the same Paulis, e.g. .. code-block:: diff --git a/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml b/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml index af5f158ecb1..5f79ace148e 100644 --- a/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml +++ b/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml @@ -12,7 +12,7 @@ features: Previously, the pass chain would have been implemented as ``scheduling -> alignment`` which were both transform passes thus there were multiple :class:`~.DAGCircuit` - instances recreated during each pass. In addition, scheduling occured in each pass + instances recreated during each pass. In addition, scheduling occurred in each pass to obtain instruction start time. Now the required pass chain becomes ``scheduling -> alignment -> padding`` where the :class:`~.DAGCircuit` update only occurs at the end with the ``padding`` pass. @@ -59,7 +59,7 @@ features: The :class:`~.ConstrainedReschedule` pass considers both hardware alignment constraints that can be definied in a :class:`.BackendConfiguration` object, ``pulse_alignment`` and ``acquire_alignment``. This new class superscedes - the previosuly existing :class:`~.AlignMeasures` as it performs the same alignment + the previously existing :class:`~.AlignMeasures` as it performs the same alignment (via the property set) for measurement instructions in addition to general instruction alignment. By setting the ``acquire_alignment`` constraint argument for the :class:`~.ConstrainedReschedule` pass it is a drop-in replacement of @@ -67,7 +67,7 @@ features: - | Added two new transpiler passes :class:`~.ALAPScheduleAnalysis` and :class:`~.ASAPScheduleAnalysis` which superscede the :class:`~.ALAPSchedule` and :class:`~.ASAPSchedule` as part of the - reworked transpiler workflow for schedling. The new passes perform the same scheduling but + reworked transpiler workflow for scheduling. The new passes perform the same scheduling but in the property set and relying on a :class:`~.BasePadding` pass to adjust the circuit based on all the scheduling alignment analysis. @@ -155,5 +155,5 @@ features: Added a new transpiler pass :class:`~.PadDynamicalDecoupling` which superscedes the :class:`~.DynamicalDecoupling` pass as part of the reworked transpiler workflow for scheduling. This new pass will insert dynamical decoupling - sequences into the circuit per any scheduling and alignment analysis that occured in earlier + sequences into the circuit per any scheduling and alignment analysis that occurred in earlier passes. diff --git a/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml b/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml index 86e9c23612a..93d39e48184 100644 --- a/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml +++ b/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml @@ -18,7 +18,7 @@ upgrade: (where ``circuit.qubits[0]`` is mapped to physical qubit 0, ``circuit.qubits[1]`` is mapped to physical qubit 1, etc) assuming the trivial layout is perfect. If your use case was dependent on the - trivial layout you can explictly request it when transpiling by specifying + trivial layout you can explicitly request it when transpiling by specifying ``layout_method="trivial"`` when calling :func:`~qiskit.compiler.transpile`. - | The preset pass manager for optimization level 1 (when calling diff --git a/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml b/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml index 361f90cf35f..182ccd32a79 100644 --- a/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml +++ b/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml @@ -3,7 +3,7 @@ features: - | Added a new function :func:`~.marginal_memory` which is used to marginalize shot memory arrays. Provided with the shot memory array and the indices - of interest, the function will return a maginized shot memory array. This + of interest, the function will return a marginalized shot memory array. This function differs from the memory support in the :func:`~.marginal_counts` method which only works on the ``memory`` field in a :class:`~.Results` object. diff --git a/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml b/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml index 5446d12e9ee..e508f5b734f 100644 --- a/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml +++ b/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml @@ -15,7 +15,7 @@ features: This pass is similar to the :class:`~.VF2Layout` pass and both internally use the same VF2 implementation from `retworkx `__. However, - :class:`~.VF2PostLayout` is deisgned to run after initial layout, routing, + :class:`~.VF2PostLayout` is designed to run after initial layout, routing, basis translation, and any optimization passes run and will only work if a layout has already been applied, the circuit has been routed, and all gates are in the target basis. This is required so that when a new layout diff --git a/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml b/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml index a6a8cbd06bd..f79c4ade2c7 100644 --- a/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml +++ b/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml @@ -2,7 +2,7 @@ features: - | The algorithm iteratively computes each eigenstate by starting from the ground - state (which is computed as in VQE) and then optimising a modified cost function + state (which is computed as in VQE) and then optimizing a modified cost function that tries to compute eigen states that are orthogonal to the states computed in the previous iterations and have the lowest energy when computed over the ansatz. The interface implemented is very similar to that of VQE and is of the form: diff --git a/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml b/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml index 5606c830b41..65d64dda08d 100644 --- a/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml +++ b/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml @@ -13,5 +13,5 @@ upgrade: :class:`~.RealAmplitudes` and :class:`~.EfficientSU2` classes has changed from ``"full"`` to ``"reverse_linear"``. This change was made because the output circuit is equivalent but uses only :math:`n-1` instead of :math:`\frac{n(n-1)}{2}` :class:`~.CXGate` gates. If you - desire the previous default you can explicity set ``entanglement="full"`` when calling either + desire the previous default you can explicitly set ``entanglement="full"`` when calling either constructor. diff --git a/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml b/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml index 7f1e448fd18..9f535dcd409 100644 --- a/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml +++ b/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml @@ -93,7 +93,7 @@ upgrade: and no edges. This change was made to better reflect the actual connectivity constraints of the :class:`~.Target` because in this case there are no connectivity constraints on the backend being modeled by - the :class:`~.Target`, not a lack of connecitvity. If you desire the + the :class:`~.Target`, not a lack of connectivity. If you desire the previous behavior for any reason you can reproduce it by checking for a ``None`` return and manually building a coupling map, for example:: diff --git a/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml b/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml index a0a56ff0d85..e471b6dc371 100644 --- a/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml +++ b/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml @@ -2,5 +2,5 @@ fixes: - | The :class:`.GateDirection` transpiler pass will now respect the available - values for gate parameters when handling parametrised gates with a + values for gate parameters when handling parametrized gates with a :class:`.Target`. diff --git a/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml b/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml index d6267404c24..4ef32d600fc 100644 --- a/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml +++ b/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml @@ -3,7 +3,7 @@ features: - | Added new methods for executing primitives: :meth:`.BaseSampler.run` and :meth:`.BaseEstimator.run`. These methods execute asynchronously and return :class:`.JobV1` objects which - provide a handle to the exections. These new run methods can be passed :class:`~.QuantumCircuit` + provide a handle to the exceptions. These new run methods can be passed :class:`~.QuantumCircuit` objects (and observables for :class:`~.BaseEstimator`) that are not registered in the constructor. For example:: diff --git a/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml b/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml index 0162dfcad6a..15bb0769884 100644 --- a/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml +++ b/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml @@ -5,7 +5,7 @@ features: :class:`.Drag` and :class:`.Constant` have been upgraded to instantiate :class:`SymbolicPulse` rather than the subclass itself. All parametric pulse objects in pulse programs must be symbolic pulse instances, - because subclassing is no longer neccesary. Note that :class:`SymbolicPulse` can + because subclassing is no longer necessary. Note that :class:`SymbolicPulse` can uniquely identify a particular envelope with the symbolic expression object defined in :attr:`SymbolicPulse.envelope`. upgrade: @@ -15,7 +15,7 @@ upgrade: these pulse subclasses are no longer instantiated. They will still work in Terra 0.22, but you should begin transitioning immediately. Instead of using type information, :attr:`SymbolicPulse.pulse_type` should be used. - This is assumed to be a unique string identifer for pulse envelopes, + This is assumed to be a unique string identifier for pulse envelopes, and we can use string equality to investigate the pulse types. For example, .. code-block:: python diff --git a/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml b/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml index 9e5c3916c78..659b7312bb1 100644 --- a/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml +++ b/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml @@ -72,7 +72,7 @@ features: evaluated_gradient = grad(ask_data.x_center) optimizer.state.njev += 1 - optmizer.state.nit += 1 + optimizer.state.nit += 1 cf = TellData(eval_jac=evaluated_gradient) optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml b/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml index 6061ff04b3d..3da0de9d6db 100644 --- a/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml +++ b/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml @@ -5,5 +5,5 @@ features: class. The implementation is restricted to mitigation patterns in which each qubit is mitigated individually, e.g. ``[[0], [1], [2]]``. This is, however, the most widely used case. It allows the :class:`.TensoredMeasFitter` to - be used in cases where the numberical order of the physical qubits does not + be used in cases where the numerical order of the physical qubits does not match the index of the classical bit. diff --git a/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml b/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml index 009984fbd7f..92c07b4e245 100644 --- a/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml +++ b/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml @@ -2,7 +2,7 @@ upgrade: - | The visualization module :mod:`qiskit.visualization` has seen some internal - reorganisation. This should not have affected the public interface, but if + reorganization. This should not have affected the public interface, but if you were accessing any internals of the circuit drawers, they may now be in different places. The only parts of the visualization module that are considered public are the components that are documented in this online diff --git a/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml b/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml index a9fa4703d67..f28178eb320 100644 --- a/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml +++ b/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml @@ -1,9 +1,9 @@ --- fixes: - | - QPY deserialisation will no longer add extra :class:`.Clbit` instances to the + QPY deserialization will no longer add extra :class:`.Clbit` instances to the circuit if there are both loose :class:`.Clbit`\ s in the circuit and more :class:`~qiskit.circuit.Qubit`\ s than :class:`.Clbit`\ s. - | - QPY deserialisation will no longer add registers named `q` and `c` if the + QPY deserialization will no longer add registers named `q` and `c` if the input circuit contained only loose bits. diff --git a/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml b/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml index e1d083a4a2d..716726dcbe5 100644 --- a/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml +++ b/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml @@ -2,7 +2,7 @@ fixes: - | Fixes issue where :meth:`.Statevector.evolve` and :meth:`.DensityMatrix.evolve` - would raise an exeception for nested subsystem evolution for non-qubit + would raise an exception for nested subsystem evolution for non-qubit subsystems. Fixes `issue #8897 `_ - | diff --git a/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml b/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml index 365c5858610..d7dbd7edcb3 100644 --- a/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml +++ b/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml @@ -3,4 +3,4 @@ upgrade: - | The ``initial_state`` argument of the :class:`~NLocal` class should be a :class:`~.QuantumCircuit`. Passing any other type was deprecated as of Qiskit - Terra 0.18.0 (July 2021) and that posibility is now removed. + Terra 0.18.0 (July 2021) and that possibility is now removed. diff --git a/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml b/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml index 969b2aa2a24..6d1d608aab7 100644 --- a/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml +++ b/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml @@ -3,6 +3,6 @@ features: - | The :class:`~.Optimize1qGatesDecomposition` transpiler pass has a new keyword argument, ``target``, on its constructor. This argument can be used to - specify a :class:`~.Target` object that represnts the compilation target. + specify a :class:`~.Target` object that represents the compilation target. If used it superscedes the ``basis`` argument to determine if an instruction in the circuit is present on the target backend. diff --git a/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml b/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml index c019f1329d0..a755d3b9506 100644 --- a/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml +++ b/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml @@ -3,6 +3,6 @@ features: - | The :class:`~.UnrollCustomDefinitions` transpiler pass has a new keyword argument, ``target``, on its constructor. This argument can be used to - specify a :class:`~.Target` object that represnts the compilation target. - If used it superscedes the ``basis_gates`` argument to determine if an + specify a :class:`~.Target` object that represents the compilation target. + If used it supersedes the ``basis_gates`` argument to determine if an instruction in the circuit is present on the target backend. diff --git a/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml b/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml index ea98eddd2d6..7f4e7a64fcd 100644 --- a/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml +++ b/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml @@ -65,7 +65,7 @@ features: qc.append(lin_fun, [0, 1, 2]) qc.append(cliff, [1, 2, 3]) - # Choose synthesis methods that adhere to linear-nearest-neighbour connectivity + # Choose synthesis methods that adhere to linear-nearest-neighbor connectivity hls_config = HLSConfig(linear_function=["kms"], clifford=["lnn"]) # Synthesize diff --git a/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml b/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml index 4072e863538..55bd079d0f8 100644 --- a/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml +++ b/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml @@ -9,5 +9,5 @@ features: * :class:``~qiskit.pulse.library.Triangle`` The new functions return a ``ScalableSymbolicPulse``. With the exception of the ``Sawtooth`` phase, - behaviour is identical to that of the corresponding waveform generators (:class:``~qiskit.pulse.library.sin`` etc). + behavior is identical to that of the corresponding waveform generators (:class:``~qiskit.pulse.library.sin`` etc). The ``Sawtooth`` phase is defined such that a phase of :math:``2\\pi`` shifts by a full cycle. diff --git a/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml b/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml index 93a0523397b..85637311dc4 100644 --- a/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml +++ b/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml @@ -10,5 +10,5 @@ deprecations: The pass was made into a separate plugin package for two reasons, first the dependency on CPLEX makes it harder to use and secondly the plugin - packge more cleanly integrates with :func:`~.transpile`. + package more cleanly integrates with :func:`~.transpile`. diff --git a/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml b/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml index e06fec8772f..c992160b740 100644 --- a/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml +++ b/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml @@ -5,4 +5,4 @@ fixes: to be any object that implements :class:`.Operation`, not just a :class:`.circuit.Instruction`. Note that any manual mutation of :attr:`.QuantumCircuit.data` is discouraged; it is not *usually* any more efficient than building a new circuit object, as checking the invariants - surrounding parametrised objects can be surprisingly expensive. + surrounding parametrized objects can be surprisingly expensive. diff --git a/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml b/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml index 000dd81ee98..c217852c33c 100644 --- a/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml +++ b/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml @@ -2,6 +2,6 @@ fixes: - | Fixed a bug in :meth:`.TensoredOp.to_matrix` where the global coefficient of the operator - was multiplied to the final matrix more than once. Now, the global coefficient is correclty + was multiplied to the final matrix more than once. Now, the global coefficient is correctly applied, independent of the number of tensored operators or states. Fixed `#9398 `__. diff --git a/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml b/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml index f33b3d240ba..79fe381d896 100644 --- a/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml +++ b/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml @@ -5,4 +5,4 @@ features: and RZXCalibrationBuilderNoEcho to consume `ecr` entangling gates from the backend, in addition to the `cx` gates they were build for. These native gates contain the calibrated pulse schedules that the pulse scaling passes use to - generate arbitraty rotations of the :class:`~RZXGate` operation. + generate arbitrary rotations of the :class:`~RZXGate` operation. diff --git a/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml b/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml index c6170b62a8f..c2003139d67 100644 --- a/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml +++ b/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml @@ -21,7 +21,7 @@ fixes: `#7769 `__ and `#7773 `__. - | - Standard gates defined by Qiskit, such as :class:`.RZXGate`, will now have properly parametrised + Standard gates defined by Qiskit, such as :class:`.RZXGate`, will now have properly parametrized definitions when exported using the OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`). See `#7172 `__. - | diff --git a/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml b/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml index f38d694e77a..0fc7dd6b69a 100644 --- a/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml +++ b/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml @@ -17,7 +17,7 @@ features: This new parser is approximately 10x faster than the existing ones at :meth:`.QuantumCircuit.from_qasm_file` and :meth:`.QuantumCircuit.from_qasm_str` for large files, - and has less overhead on each call as well. The new parser is more extensible, customisable and + and has less overhead on each call as well. The new parser is more extensible, customizable and generally also more type-safe; it will not attempt to output custom Qiskit objects when the definition in the OpenQASM 2 file clashes with the Qiskit object, unlike the current exporter. See the :mod:`qiskit.qasm2` module documentation for full details and more examples. diff --git a/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml b/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml index fadd99c8023..9af85d8572d 100644 --- a/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml +++ b/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml @@ -5,6 +5,6 @@ features: to pass a list of optimizers and initial points for the different minimization runs. For example, the ``k``-th initial point and ``k``-th optimizer will be used for the optimization of the - ``k-1``-th exicted state. + ``k-1``-th excited state. diff --git a/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml b/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml index d5dfd69f54a..999d30095d1 100644 --- a/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml +++ b/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml @@ -3,7 +3,7 @@ features: - | :meth:`.DAGCircuit.substitute_node` gained a ``propagate_condition`` keyword argument that is analogous to the same argument in :meth:`~.DAGCircuit.substitute_node_with_dag`. Setting this - to ``False`` opts out of the legacy behaviour of copying a condition on the ``node`` onto the + to ``False`` opts out of the legacy behavior of copying a condition on the ``node`` onto the new ``op`` that is replacing it. This option is ignored for general control-flow operations, which will never propagate their diff --git a/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml b/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml index c8f7e3ddd8c..4902fb85a2f 100644 --- a/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml +++ b/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml @@ -15,6 +15,6 @@ features: reduce the overhead of input normalisation in this function. fixes: - | - A parametrised circuit that contains a custom gate whose definition has a parametrised global phase + A parametrized circuit that contains a custom gate whose definition has a parametrized global phase can now successfully bind the parameter in the inner global phase. See `#10283 `__ for more detail. diff --git a/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml b/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml index aa89abeb662..30d76baa436 100644 --- a/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml +++ b/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml @@ -4,4 +4,4 @@ fixes: Fixed the gate decomposition of multi-controlled Z rotation gates added via :meth:`.QuantumCircuit.mcrz`. Previously, this method implemented a multi-controlled phase gate, which has a relative phase difference to the Z rotation. To obtain the - previous `.QuantumCircuit.mcrz` behaviour, use `.QuantumCircuit.mcp`. + previous `.QuantumCircuit.mcrz` behavior, use `.QuantumCircuit.mcp`. diff --git a/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml b/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml index 890223e82b8..40975c31d1d 100644 --- a/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml +++ b/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml @@ -3,5 +3,5 @@ fixes: - | When the parameter ``conditional=True`` is set in ``qiskit.circuit.random.random_circuit``, the conditional operations will - be preceded by a full mid-circuit measurment. + be preceded by a full mid-circuit measurement. Fixes `#9016 `__ diff --git a/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml b/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml index ebb699e8c7b..246387384ca 100644 --- a/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml +++ b/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml @@ -17,6 +17,6 @@ features: :class:`~.circuit.Instruction` objects. While this isn't optimal for visualization it typically results in much better runtime performance, especially with :meth:`.QuantumCircuit.bind_parameters` and - :meth:`.QuantumCircuit.assign_parameters` which can see a substatial + :meth:`.QuantumCircuit.assign_parameters` which can see a substantial runtime improvement with a flattened output compared to the nested wrapped default output. diff --git a/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml b/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml index e460b6e55ee..6615b11605e 100644 --- a/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml +++ b/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml @@ -4,5 +4,5 @@ features: The instructions :class:`.StatePreparation` and :class:`~.extensions.Initialize`, and their associated circuit methods :meth:`.QuantumCircuit.prepare_state` and :meth:`~.QuantumCircuit.initialize`, gained a keyword argument ``normalize``, which can be set to ``True`` to automatically normalize - an array target. By default this is ``False``, which retains the current behaviour of + an array target. By default this is ``False``, which retains the current behavior of raising an exception when given non-normalized input. diff --git a/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml b/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml index 35ce0ce4ca5..9ca56d961bc 100644 --- a/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml +++ b/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml @@ -7,6 +7,6 @@ upgrade: fixes: - | Fixed the :mod:`~qiskit.qpy` serialization of :attr:`.QuantumCircuit.layout` - attribue. Previously, the :attr:`~.QuantumCircuit.layout` attribute would + attribute. Previously, the :attr:`~.QuantumCircuit.layout` attribute would have been dropped when serializing a circuit to QPY. Fixed `#10112 `__ diff --git a/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml b/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml index fdfaf394e8b..844deea0b80 100644 --- a/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml +++ b/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml @@ -2,5 +2,5 @@ upgrade: - | The :meth:`~.ApproximateTokenSwapper.map` has been modified to use the new ``rustworkx`` version - of :func:`~graph_token_swapper` for performance reasons. Qiskit Terra 0.25 now requires versison + of :func:`~graph_token_swapper` for performance reasons. Qiskit Terra 0.25 now requires version 0.13.0 of ``rustworkx``. diff --git a/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml b/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml index 4126b91bb86..17bf77f51c1 100644 --- a/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml +++ b/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml @@ -4,5 +4,5 @@ features: The :class:`.DAGCircuit` methods :meth:`~.DAGCircuit.apply_operation_back` and :meth:`~.DAGCircuit.apply_operation_front` have gained a ``check`` keyword argument that can be set ``False`` to skip validation that the inputs uphold the :class:`.DAGCircuit` data-structure - invariants. This is useful as a performance optimisation when the DAG is being built from + invariants. This is useful as a performance optimization when the DAG is being built from known-good data, such as during transpiler passes. diff --git a/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml b/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml index a62ed79a047..a55582f9476 100644 --- a/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml +++ b/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml @@ -13,5 +13,5 @@ deprecations: * :meth:`.QuantumCircuit.i` in favor of :meth:`.QuantumCircuit.id` Note that :meth:`.QuantumCircuit.i` is the only exception to the rule above, but since - :meth:`.QuantumCircuit.id` more intuively represents the identity and is used more, we chose + :meth:`.QuantumCircuit.id` more intuitively represents the identity and is used more, we chose it over its counterpart. \ No newline at end of file diff --git a/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml b/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml index c3d4bbe6386..2819be565c1 100644 --- a/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml +++ b/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml @@ -4,5 +4,5 @@ fixes: The :class:`.GateDirection` transpiler pass will now use discrete-basis translations rather than relying on a continuous :class:`.RYGate`, which should help make some discrete-basis-set targets slightly more reliable. In general, :func:`.transpile` only has partial support for basis sets - that do not contain a continuously-parametrised operation, and so it may not always succeed in + that do not contain a continuously-parametrized operation, and so it may not always succeed in these situations, and will almost certainly not produce optimal results. diff --git a/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml b/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml index 234888b2ac9..335c819dc8c 100644 --- a/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml +++ b/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml @@ -60,7 +60,7 @@ features: and :class:`.Clbit` instances. All these classical expressions are fully supported through the Qiskit transpiler stack, through - QPY serialisation (:mod:`qiskit.qpy`) and for export to OpenQASM 3 (:mod:`qiskit.qasm3`). Import + QPY serialization (:mod:`qiskit.qpy`) and for export to OpenQASM 3 (:mod:`qiskit.qasm3`). Import from OpenQASM 3 is currently managed by `a separate package `__ (which is re-exposed via :mod:`qiskit.qasm3`), which we hope will be extended to match the new features in Qiskit. diff --git a/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml b/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml index e03fa8555a4..8f04a25141e 100644 --- a/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml +++ b/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml @@ -3,8 +3,8 @@ features: - | :class:`.Parameter` now has an advanced-usage keyword argument ``uuid`` in its constructor, which can be used to make the :class:`.Parameter` compare equal to another of the same name. - This should not typically be used by users, and is most useful for custom serialisation and - deserialisation. + This should not typically be used by users, and is most useful for custom serialization and + deserialization. fixes: - | The hash of a :class:`.Parameter` is now equal to the hashes of any diff --git a/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml b/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml index e93e62c963a..e75c8e09606 100644 --- a/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml +++ b/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml @@ -4,7 +4,7 @@ deprecations: Passing a circuit to :func:`qiskit.visualization.timeline_drawer` that does not have scheduled node start-time information is deprecated. Only circuits that have gone through one of the scheduling analysis passes (for example :class:`.ALAPScheduleAnalysis` or - :class:`.ASAPScheduleAnalysis`) can be visualised. If you have used one of the old-style + :class:`.ASAPScheduleAnalysis`) can be visualized. If you have used one of the old-style scheduling passes (for example :class:`.ALAPSchedule` or :class:`.ASAPSchedule`), you can propagate the scheduling information by running:: @@ -18,5 +18,5 @@ deprecations: instruction_durations=InstructionDurations(), ) - This behaviour was previously intended to be deprecated in Qiskit 0.37, but due to a bug in the - warning, it was not displayed to users until now. The behaviour will be removed in Qiskit 1.0. + This behavior was previously intended to be deprecated in Qiskit 0.37, but due to a bug in the + warning, it was not displayed to users until now. The behavior will be removed in Qiskit 1.0. diff --git a/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml b/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml index befe3011c70..a1e4a549266 100644 --- a/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml +++ b/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml @@ -18,7 +18,7 @@ features: (:mod:`qiskit.qasm3`) and QPY (:mod:`qiskit.qpy`) modules. This is particularly important since the method name :meth:`~.QuantumCircuit.qasm` gave no indication of the OpenQASM version, and since it was originally - added, Qiskit has gained several serialisation modules that could easily + added, Qiskit has gained several serialization modules that could easily become confused. deprecations: - | diff --git a/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml index 7b8313e6b08..ac38751d9ce 100644 --- a/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml @@ -103,7 +103,7 @@ features: :attr:`.Instruction.mutable` which is used to get a mutable copy and check whether an :class:`~.circuit.Instruction` object is mutable. With the introduction of :class:`~.SingletonGate` these methods can be used to have a unified interface - to deal with the mutablitiy of instruction objects. + to deal with the mutability of instruction objects. - | Added an attribute :attr:`.Instruction.base_class`, which gets the "base" type of an instruction. Many instructions will satisfy ``type(obj) == obj.base_class``, however the diff --git a/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml b/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml index 151127a0c8d..a6a8745d175 100644 --- a/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml +++ b/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml @@ -121,7 +121,7 @@ other: ``qiskit.execute()`` has been changed to optimization level 1 pass manager defined at ``qiskit.transpile.preset_passmanagers.level1_pass_manager``. - | - All the circuit drawer backends now willl express gate parameters in a + All the circuit drawer backends now will express gate parameters in a circuit as common fractions of pi in the output visualization. If the value of a parameter can be expressed as a fraction of pi that will be used instead of the numeric equivalent. diff --git a/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml b/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml index 925da8840f5..30b7baca783 100644 --- a/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml +++ b/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml @@ -38,7 +38,7 @@ features: Two new functions, ``sech()`` and ``sech_deriv()`` were added to the pulse library module ``qiskit.pulse.pulse_lib`` for creating an unnormalized hyperbolic secant ``SamplePulse`` object and an unnormalized hyperbolic - secant derviative ``SamplePulse`` object respectively. + secant derivative ``SamplePulse`` object respectively. - | A new kwarg option ``vertical_compression`` was added to the ``QuantumCircuit.draw()`` method and the @@ -61,7 +61,7 @@ features: - | When creating a PassManager you can now specify a callback function that if specified will be run after each pass is executed. This function gets - passed a set of kwargs on each call with the state of the pass maanger after + passed a set of kwargs on each call with the state of the pass manager after each pass execution. Currently these kwargs are: * pass\_ (Pass): the pass being run diff --git a/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml b/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml index 62626e80995..1b11fe94542 100644 --- a/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml +++ b/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml @@ -8,7 +8,7 @@ upgrade_algorithms: - | - A series of legacy quantum execution utililties have been removed, following their deprecation in Qiskit 0.44. + A series of legacy quantum execution utilities have been removed, following their deprecation in Qiskit 0.44. These include the ``qiskit.utils.QuantumInstance`` class, as well the modules: - ``qiskit.utils.backend_utils`` diff --git a/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml b/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml index a86869a4e54..62e7e945161 100644 --- a/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml +++ b/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml @@ -1,3 +1,3 @@ fixes: - | - Fix incorrect implemention of `qDRIFT`, negative coeffients of the Hamiltonian are now added back whereas they were always forced to be positive. + Fix incorrect implemention of `qDRIFT`, negative coefficients of the Hamiltonian are now added back whereas they were always forced to be positive. diff --git a/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml b/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml index 0bf60329a23..ff83deee939 100644 --- a/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml +++ b/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml @@ -3,7 +3,7 @@ features: - | Added a new transpiler pass :class:`.StarPreRouting` which is designed to identify star connectivity subcircuits and then replace them with an optimal linear routing. This is useful for certain circuits that are composed of - this circuit connectivity such as Berstein Vazirani and QFT. For example: + this circuit connectivity such as Bernstein Vazirani and QFT. For example: .. plot: diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml index 73da8e6b7ad..6fa548d9245 100644 --- a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -5,7 +5,7 @@ features: :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the user to pass single bitstring to measure an outcome for. Previouslly the :meth:'~.StabilizerState.probabilities_dict' would be utilized and would - at worst case calculate (2^n) number of probabilbity calculations (depending + at worst case calculate (2^n) number of probability calculations (depending on the state), even if a user wanted a single result. With this new method the user can calculate just the single outcome bitstring value a user passes to measure the probability for. As the number of qubits increases, the more diff --git a/setup.py b/setup.py index 38af5286e81..61168050547 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ # # python setup.py build_rust --inplace --release # -# to make optimised Rust components even for editable releases, which would otherwise be quite +# to make optimized Rust components even for editable releases, which would otherwise be quite # unergonomic to do otherwise. diff --git a/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm b/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm index 4910c35dfec..ba5db139704 100644 --- a/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm +++ b/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm @@ -1,7 +1,7 @@ // Originally source from the QUEKO benchmark suite // https://github.com/UCLA-VAST/QUEKO-benchmark // A benchmark that is near-term feasible for Google Sycamore with a optimal -// soluation depth of 25 +// solution depth of 25 OPENQASM 2.0; include "qelib1.inc"; qreg q[54]; diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 10cef88122c..012697a17dd 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -224,7 +224,7 @@ def test_binary_bitwise_explicit(self, function, opcode): ) @ddt.unpack def test_binary_bitwise_uint_inference(self, function, opcode): - """The binary bitwise functions have specialised inference for the widths of integer + """The binary bitwise functions have specialized inference for the widths of integer literals, since the bitwise functions require the operands to already be of exactly the same width without promotion.""" cr = ClassicalRegister(8, "c") @@ -247,7 +247,7 @@ def test_binary_bitwise_uint_inference(self, function, opcode): ), ) - # Inference between two integer literals is "best effort". This behaviour isn't super + # Inference between two integer literals is "best effort". This behavior isn't super # important to maintain if we want to change the expression system. self.assertEqual( function(5, 255), diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 6e1dcf77e1e..625db22cc12 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -85,7 +85,7 @@ def test_var_equality(self): self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool())) # Manually constructing the same object with the same UUID should cause it compare equal, - # though, for serialisation ease. + # though, for serialization ease. self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a")) # This is a badly constructed variable because it's using a different type to refer to the diff --git a/test/python/circuit/library/test_diagonal.py b/test/python/circuit/library/test_diagonal.py index 7dde0d62d8f..b1158fdab3f 100644 --- a/test/python/circuit/library/test_diagonal.py +++ b/test/python/circuit/library/test_diagonal.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the digonal circuit.""" +"""Test the diagonal circuit.""" import unittest from ddt import ddt, data diff --git a/test/python/circuit/library/test_qft.py b/test/python/circuit/library/test_qft.py index 3d85bb526dc..078b5af04ea 100644 --- a/test/python/circuit/library/test_qft.py +++ b/test/python/circuit/library/test_qft.py @@ -183,7 +183,7 @@ def __init__(self, *_args, **_kwargs): raise self # We don't want to issue a warning on mutation until we know that the values are - # finalised; this is because a user might want to mutate the number of qubits and the + # finalized; this is because a user might want to mutate the number of qubits and the # approximation degree. In these cases, wait until we try to build the circuit. with warnings.catch_warnings(record=True) as caught_warnings: warnings.filterwarnings( diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 216ea3a59bb..04e71a0dd4d 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1163,7 +1163,7 @@ def test_qpy_with_for_loop_iterator(self): self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_clbit_switch(self): - """Test QPY serialisation for a switch statement with a Clbit target.""" + """Test QPY serialization for a switch statement with a Clbit target.""" case_t = QuantumCircuit(2, 1) case_t.x(0) case_f = QuantumCircuit(2, 1) @@ -1180,7 +1180,7 @@ def test_qpy_clbit_switch(self): self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_register_switch(self): - """Test QPY serialisation for a switch statement with a ClassicalRegister target.""" + """Test QPY serialization for a switch statement with a ClassicalRegister target.""" qreg = QuantumRegister(2, "q") creg = ClassicalRegister(3, "c") diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index e9a7416f78c..517a7093e81 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -928,7 +928,7 @@ def test_remove_final_measurements_7089(self): self.assertEqual(circuit.clbits, []) def test_remove_final_measurements_bit_locations(self): - """Test remove_final_measurements properly recalculates clbit indicies + """Test remove_final_measurements properly recalculates clbit indices and preserves order of remaining cregs and clbits. """ c0 = ClassicalRegister(1) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index 8b7167eed7e..f6916dcb72d 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -76,7 +76,7 @@ def test_initialise_declarations_mapping(self): ) def test_initialise_declarations_dependencies(self): - """Test that the cirucit initialiser can take in declarations with dependencies between + """Test that the circuit initializer can take in declarations with dependencies between them, provided they're specified in a suitable order.""" a = expr.Var.new("a", types.Bool()) vars_ = [ diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 0aeeb084f47..e41b5c1f9f3 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -1438,7 +1438,7 @@ def test_break_continue_deeply_nested(self, loop_operation): These are the deepest tests, hitting all parts of the deferred builder scopes. We test ``if``, ``if/else`` and ``switch`` paths at various levels of the scoping to try and account - for as many weird edge cases with the deferred behaviour as possible. We try to make sure, + for as many weird edge cases with the deferred behavior as possible. We try to make sure, particularly in the most complicated examples, that there are resources added before and after every single scope, to try and catch all possibilities of where resources may be missed. @@ -2943,7 +2943,7 @@ def test_inplace_compose_within_builder(self): self.assertEqual(canonicalize_control_flow(outer), canonicalize_control_flow(expected)) def test_global_phase_of_blocks(self): - """It should be possible to set a global phase of a scope independantly of the containing + """It should be possible to set a global phase of a scope independently of the containing scope and other sibling scopes.""" qr = QuantumRegister(3) cr = ClassicalRegister(3) @@ -3335,7 +3335,7 @@ def test_if_rejects_break_continue_if_not_in_loop(self): def test_for_rejects_reentry(self): """Test that the ``for``-loop context manager rejects attempts to re-enter it. Since it holds some forms of state during execution (the loop variable, which may be generated), we - can't safely re-enter it and get the expected behaviour.""" + can't safely re-enter it and get the expected behavior.""" for_manager = QuantumCircuit(2, 2).for_loop(range(2)) with for_manager: @@ -3584,7 +3584,7 @@ def test_reject_c_if_from_outside_scope(self): # As a side-effect of how the lazy building of 'if' statements works, we actually # *could* add a condition to the gate after the 'if' block as long as we were still # within the 'for' loop. It should actually manage the resource correctly as well, but - # it's "undefined behaviour" than something we specifically want to forbid or allow. + # it's "undefined behavior" than something we specifically want to forbid or allow. test = QuantumCircuit(bits) with test.for_loop(range(2)): with test.if_test(cond): diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index ced7229415e..8ba70ee852c 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -1262,7 +1262,7 @@ def test_modify_cugate_params_slice(self): self.assertEqual(cu.base_gate.params, [0.4, 0.3, 0.2]) def test_assign_nested_controlled_cu(self): - """Test assignment of an arbitrary controlled parametrised gate that appears through the + """Test assignment of an arbitrary controlled parametrized gate that appears through the `Gate.control()` method on an already-controlled gate.""" theta = Parameter("t") qc_c = QuantumCircuit(2) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index dbda9262f15..170b47632c4 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -566,7 +566,7 @@ def case(specifier, message): case(1.0, r"Unknown classical resource specifier: .*") def test_instructionset_c_if_with_no_requester(self): - """Test that using a raw :obj:`.InstructionSet` with no classical-resource resoluer accepts + """Test that using a raw :obj:`.InstructionSet` with no classical-resource resolver accepts arbitrary :obj:`.Clbit` and `:obj:`.ClassicalRegister` instances, but rejects integers.""" with self.subTest("accepts arbitrary register"): diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index feab3002f58..0095f87be9a 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -314,7 +314,7 @@ def test_assign_parameters_by_name(self): ) def test_bind_parameters_custom_definition_global_phase(self): - """Test that a custom gate with a parametrised `global_phase` is assigned correctly.""" + """Test that a custom gate with a parametrized `global_phase` is assigned correctly.""" x = Parameter("x") custom = QuantumCircuit(1, global_phase=x).to_gate() base = QuantumCircuit(1) @@ -1485,7 +1485,7 @@ def test_cast_to_float_when_underlying_expression_bound(self): def test_cast_to_float_intermediate_complex_value(self): """Verify expression can be cast to a float when it is fully bound, but an intermediate part of the expression evaluation involved complex types. Sympy is generally more permissive - than symengine here, and sympy's tends to be the expected behaviour for our users.""" + than symengine here, and sympy's tends to be the expected behavior for our users.""" x = Parameter("x") bound_expr = (x + 1.0 + 1.0j).bind({x: -1.0j}) self.assertEqual(float(bound_expr), 1.0) @@ -2166,7 +2166,7 @@ def test_parameter_equal_to_identical_expression(self): self.assertEqual(theta, expr) def test_parameter_symbol_equal_after_ufunc(self): - """Verfiy ParameterExpression phi + """Verify ParameterExpression phi and ParameterExpression cos(phi) have the same symbol map""" phi = Parameter("phi") cos_phi = numpy.cos(phi) diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 630db1c0c1c..1a6e5b6b8fe 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -233,7 +233,7 @@ def test_assemble_opaque_inst(self): self.assertEqual(qobj.experiments[0].instructions[0].params, [0.5, 0.4]) def test_assemble_unroll_parametervector(self): - """Verfiy that assemble unrolls parametervectors ref #5467""" + """Verify that assemble unrolls parametervectors ref #5467""" pv1 = ParameterVector("pv1", 3) pv2 = ParameterVector("pv2", 3) qc = QuantumCircuit(2, 2) @@ -609,7 +609,7 @@ def test_pulse_gates_common_cals(self): self.assertFalse(hasattr(qobj.experiments[1].config, "calibrations")) def test_assemble_adds_circuit_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the exeriment header.""" + """Verify that any circuit metadata is added to the experiment header.""" circ = QuantumCircuit(2, metadata={"experiment_type": "gst", "execution_number": "1234"}) qobj = assemble(circ, shots=100, memory=False, seed_simulator=6) self.assertEqual( @@ -943,7 +943,7 @@ def setUp(self): self.header = {"backend_name": "FakeOpenPulse2Q", "backend_version": "0.0.0"} def test_assemble_adds_schedule_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the exeriment header.""" + """Verify that any circuit metadata is added to the experiment header.""" self.schedule.metadata = {"experiment_type": "gst", "execution_number": "1234"} qobj = assemble( self.schedule, diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 6058f922e17..77b63a3098b 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -499,7 +499,7 @@ def test_transpile_bell(self): self.assertIsInstance(circuits, QuantumCircuit) def test_transpile_bell_discrete_basis(self): - """Test that it's possible to transpile a very simple circuit to a discrete stabiliser-like + """Test that it's possible to transpile a very simple circuit to a discrete stabilizer-like basis. In general, we do not make any guarantees about the possibility or quality of transpilation in these situations, but this is at least useful as a check that stuff that _could_ be possible remains so.""" @@ -1890,7 +1890,7 @@ def test_transpile_control_flow_no_backend(self, opt_level): @data(0, 1, 2, 3) def test_transpile_with_custom_control_flow_target(self, opt_level): - """Test transpile() with a target and constrol flow ops.""" + """Test transpile() with a target and control flow ops.""" target = GenericBackendV2(num_qubits=8, control_flow=True).target circuit = QuantumCircuit(6, 1) @@ -1927,7 +1927,7 @@ def test_transpile_with_custom_control_flow_target(self, opt_level): transpiled = transpile( circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 ) - # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # Tests of the complete validity of a circuit are mostly done at the individual pass level; # here we're just checking that various passes do appear to have run. self.assertIsInstance(transpiled, QuantumCircuit) # Assert layout ran. diff --git a/test/python/converters/test_circuit_to_instruction.py b/test/python/converters/test_circuit_to_instruction.py index d4b69e71aa1..e3239d4b5ff 100644 --- a/test/python/converters/test_circuit_to_instruction.py +++ b/test/python/converters/test_circuit_to_instruction.py @@ -226,7 +226,7 @@ def test_forbids_captured_vars(self): qc.to_instruction() def test_forbids_input_vars(self): - """This test can be relaxed when we have proper support for the behaviour. + """This test can be relaxed when we have proper support for the behavior. This actually has a natural meaning; the input variables could become typed parameters. We don't have a formal structure for managing that yet, though, so it's forbidden until the @@ -236,7 +236,7 @@ def test_forbids_input_vars(self): qc.to_instruction() def test_forbids_declared_vars(self): - """This test can be relaxed when we have proper support for the behaviour. + """This test can be relaxed when we have proper support for the behavior. This has a very natural representation, which needs basically zero special handling, since the variables are necessarily entirely internal to the subroutine. The reason it is diff --git a/test/python/dagcircuit/test_collect_blocks.py b/test/python/dagcircuit/test_collect_blocks.py index 2fe3e4bad7b..b2715078d7f 100644 --- a/test/python/dagcircuit/test_collect_blocks.py +++ b/test/python/dagcircuit/test_collect_blocks.py @@ -163,7 +163,7 @@ def test_collect_and_split_gates_from_dagcircuit(self): self.assertEqual(len(split_blocks), 3) def test_collect_and_split_gates_from_dagdependency(self): - """Test collecting and splitting blocks from DAGDependecy.""" + """Test collecting and splitting blocks from DAGDependency.""" qc = QuantumCircuit(6) qc.cx(0, 1) qc.cx(3, 5) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 0fcff29e5b4..4ab4e392cbb 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -853,7 +853,7 @@ def test_quantum_successors(self): self.assertIsInstance(cnot_node.op, CXGate) successor_cnot = self.dag.quantum_successors(cnot_node) - # Ordering between Reset and out[q1] is indeterminant. + # Ordering between Reset and out[q1] is indeterminate. successor1 = next(successor_cnot) successor2 = next(successor_cnot) @@ -902,7 +902,7 @@ def test_quantum_predecessors(self): self.assertIsInstance(cnot_node.op, CXGate) predecessor_cnot = self.dag.quantum_predecessors(cnot_node) - # Ordering between Reset and in[q1] is indeterminant. + # Ordering between Reset and in[q1] is indeterminate. predecessor1 = next(predecessor_cnot) predecessor2 = next(predecessor_cnot) @@ -1611,7 +1611,7 @@ def setUp(self): qc.h(0) qc.measure(0, 0) # The depth of an if-else is the path through the longest block (regardless of the - # condition). The size is the sum of both blocks (mostly for optimisation-target purposes). + # condition). The size is the sum of both blocks (mostly for optimization-target purposes). with qc.if_test((qc.clbits[0], True)) as else_: qc.x(1) qc.cx(2, 3) @@ -2420,11 +2420,11 @@ def test_raise_if_var_mismatch(self): def test_raise_if_substituting_dag_modifies_its_conditional(self): """Verify that we raise if the input dag modifies any of the bits in node.op.condition.""" - # The `propagate_condition=True` argument (and behaviour of `substitute_node_with_dag` + # The `propagate_condition=True` argument (and behavior of `substitute_node_with_dag` # before the parameter was added) treats the replacement DAG as implementing only the # un-controlled operation. The original contract considers it an error to replace a node # with an operation that may modify one of the condition bits in case this affects - # subsequent operations, so when `propagate_condition=True`, this error behaviour is + # subsequent operations, so when `propagate_condition=True`, this error behavior is # maintained. instr = Instruction("opaque", 1, 1, []) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index 5a8513a5ed9..ea51718aebe 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -112,7 +112,7 @@ def test_coerce_observable_zero_sparse_pauli_op(self): self.assertEqual(obs["Z"], 1) def test_coerce_observable_duplicate_sparse_pauli_op(self): - """Test coerce_observable for SparsePauliOp wiht duplicate paulis""" + """Test coerce_observable for SparsePauliOp with duplicate paulis""" op = qi.SparsePauliOp(["XX", "-XX", "XX", "-XX"], [2, 1, 3, 2]) obs = ObservablesArray.coerce_observable(op) self.assertIsInstance(obs, dict) diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index 80045dee0d6..535841cc90f 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -346,9 +346,9 @@ class TestObservableValidation(QiskitTestCase): ), ) @unpack - def test_validate_observables(self, obsevables, expected): - """Test obsevables standardization.""" - self.assertEqual(validation._validate_observables(obsevables), expected) + def test_validate_observables(self, observables, expected): + """Test observables standardization.""" + self.assertEqual(validation._validate_observables(observables), expected) @data(None, "ERROR") def test_qiskit_error(self, observables): @@ -358,7 +358,7 @@ def test_qiskit_error(self, observables): @data((), []) def test_value_error(self, observables): - """Test value error if no obsevables are provided.""" + """Test value error if no observables are provided.""" with self.assertRaises(ValueError): validation._validate_observables(observables) diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 117ead6717a..1ed2d42e0e3 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -276,7 +276,7 @@ def test_precision_seed(self): result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.901141473854881]) np.testing.assert_allclose(result[1].data.evs, [1.901141473854881]) - # precision=0 impliese the exact expectation value + # precision=0 implies the exact expectation value job = estimator.run([(psi1, hamiltonian1, [theta1])], precision=0) result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956]) diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index bf56f980a72..67628ba845a 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -342,7 +342,7 @@ def test_sequenced_parameterized_schedule(self): self.assertEqual(sched.instructions[2][-1].phase, 3) def test_schedule_generator(self): - """Test schedule generator functionalty.""" + """Test schedule generator functionality.""" dur_val = 10 amp = 1.0 @@ -364,7 +364,7 @@ def test_func(dur: int): self.assertEqual(inst_map.get_parameters("f", (0,)), ("dur",)) def test_schedule_generator_supports_parameter_expressions(self): - """Test expression-based schedule generator functionalty.""" + """Test expression-based schedule generator functionality.""" t_param = Parameter("t") amp = 1.0 diff --git a/test/python/pulse/test_reference.py b/test/python/pulse/test_reference.py index c33d7588e92..3d760346175 100644 --- a/test/python/pulse/test_reference.py +++ b/test/python/pulse/test_reference.py @@ -87,7 +87,7 @@ def test_refer_schedule_parameter_scope(self): self.assertEqual(sched_z1.parameters, sched_y1.parameters) def test_refer_schedule_parameter_assignment(self): - """Test assigning to parametr in referenced schedule""" + """Test assigning to parameter in referenced schedule""" param = circuit.Parameter("name") with pulse.build() as sched_x1: @@ -197,7 +197,7 @@ def test_calling_similar_schedule(self): """Test calling schedules with the same representation. sched_x1 and sched_y1 are the different subroutines, but same representation. - Two references shoud be created. + Two references should be created. """ param1 = circuit.Parameter("param") param2 = circuit.Parameter("param") @@ -539,7 +539,7 @@ def test_lazy_ecr(self): def test_cnot(self): """Integration test with CNOT schedule construction.""" - # echeod cross resonance + # echoed cross resonance with pulse.build(name="ecr", default_alignment="sequential") as ecr_sched: pulse.call(self.cr_sched, name="cr") pulse.call(self.xp_sched, name="xp") diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 135e874be48..048c5d7852b 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -495,7 +495,7 @@ def test_unbound_circuit(self): self.assertEqual(Exporter().dumps(qc), expected_qasm) def test_unknown_parameterized_gate_called_multiple_times(self): - """Test that a parameterised gate is called correctly if the first instance of it is + """Test that a parameterized gate is called correctly if the first instance of it is generic.""" x, y = Parameter("x"), Parameter("y") qc = QuantumCircuit(2) @@ -1310,7 +1310,7 @@ def test_chain_else_if(self): "", ] ) - # This is not the default behaviour, and it's pretty buried how you'd access it. + # This is not the default behavior, and it's pretty buried how you'd access it. builder = QASM3Builder( qc, includeslist=("stdgates.inc",), @@ -1370,7 +1370,7 @@ def test_chain_else_if_does_not_chain_if_extra_instructions(self): "", ] ) - # This is not the default behaviour, and it's pretty buried how you'd access it. + # This is not the default behavior, and it's pretty buried how you'd access it. builder = QASM3Builder( qc, includeslist=("stdgates.inc",), @@ -1935,7 +1935,7 @@ def test_var_naming_clash_gate(self): class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCase): """Test functionality that is not what we _want_, but is what we need to do while the definition - of custom gates with parameterisation does not work correctly. + of custom gates with parameterization does not work correctly. These tests are modified versions of those marked with the `requires_fixed_parameterisation` decorator, and this whole class can be deleted once those are fixed. See gh-7335. diff --git a/test/python/qasm3/test_import.py b/test/python/qasm3/test_import.py index 85da3f41933..522f68c8956 100644 --- a/test/python/qasm3/test_import.py +++ b/test/python/qasm3/test_import.py @@ -13,7 +13,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring # Since the import is nearly entirely delegated to an external package, most of the testing is done -# there. Here we need to test our wrapping behaviour for base functionality and exceptions. We +# there. Here we need to test our wrapping behavior for base functionality and exceptions. We # don't want to get into a situation where updates to `qiskit_qasm3_import` breaks Terra's test # suite due to too specific tests on the Terra side. diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py index 0ef7079f461..8c96f63c4dd 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -2119,7 +2119,7 @@ def qubitwise_commutes(left: Pauli, right: Pauli) -> bool: pauli_list = PauliList(input_labels) groups = pauli_list.group_qubit_wise_commuting() - # checking that every input Pauli in pauli_list is in a group in the ouput + # checking that every input Pauli in pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group] self.assertListEqual(sorted(output_labels), sorted(input_labels)) @@ -2153,7 +2153,7 @@ def commutes(left: Pauli, right: Pauli) -> bool: # if qubit_wise=True, equivalent to test_group_qubit_wise_commuting groups = pauli_list.group_commuting(qubit_wise=False) - # checking that every input Pauli in pauli_list is in a group in the ouput + # checking that every input Pauli in pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group] self.assertListEqual(sorted(output_labels), sorted(input_labels)) # Within each group, every operator commutes with every other operator. diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index e7a9b89b731..c4f09ec2d79 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -181,7 +181,7 @@ def test_from_index_list(self): self.assertEqual(spp_op.paulis, PauliList(expected_labels)) def test_from_index_list_parameters(self): - """Test from_list method specifying the Paulis via indices with paramteres.""" + """Test from_list method specifying the Paulis via indices with parameters.""" expected_labels = ["XXZ", "IXI", "YIZ", "III"] paulis = ["XXZ", "X", "YZ", ""] indices = [[2, 1, 0], [1], [2, 0], []] @@ -1028,7 +1028,7 @@ def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool: coeffs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j sparse_pauli_list = SparsePauliOp(input_labels, coeffs) groups = sparse_pauli_list.group_commuting(qubit_wise) - # checking that every input Pauli in sparse_pauli_list is in a group in the ouput + # checking that every input Pauli in sparse_pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group.paulis] self.assertListEqual(sorted(output_labels), sorted(input_labels)) # checking that every coeffs are grouped according to sparse_pauli_list group @@ -1056,7 +1056,7 @@ def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool: ) def test_dot_real(self): - """Test dot for real coefficiets.""" + """Test dot for real coefficients.""" x = SparsePauliOp("X", np.array([1])) y = SparsePauliOp("Y", np.array([1])) iz = SparsePauliOp("Z", 1j) diff --git a/test/python/result/test_mitigators.py b/test/python/result/test_mitigators.py index 66662bb587e..3b3e83bce00 100644 --- a/test/python/result/test_mitigators.py +++ b/test/python/result/test_mitigators.py @@ -215,7 +215,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - f"Mitigator {mitigator} did not correctly marganalize for qubits 1,2", + f"Mitigator {mitigator} did not correctly marginalize for qubits 1,2", ) mitigated_probs_02 = ( @@ -227,7 +227,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - f"Mitigator {mitigator} did not correctly marganalize for qubits 0,2", + f"Mitigator {mitigator} did not correctly marginalize for qubits 0,2", ) def test_qubits_parameter(self): diff --git a/test/python/result/test_result.py b/test/python/result/test_result.py index 7d73ab2ebcf..ff1f4cbf29a 100644 --- a/test/python/result/test_result.py +++ b/test/python/result/test_result.py @@ -105,7 +105,7 @@ def test_counts_duplicate_name(self): result.get_counts("foo") def test_result_repr(self): - """Test that repr is contstructed correctly for a results object.""" + """Test that repr is constructed correctly for a results object.""" raw_counts = {"0x0": 4, "0x2": 10} data = models.ExperimentResultData(counts=raw_counts) exp_result_header = QobjExperimentHeader( diff --git a/test/python/synthesis/test_clifford_decompose_layers.py b/test/python/synthesis/test_clifford_decompose_layers.py index 19183ce1730..1db810621e3 100644 --- a/test/python/synthesis/test_clifford_decompose_layers.py +++ b/test/python/synthesis/test_clifford_decompose_layers.py @@ -53,7 +53,7 @@ def test_decompose_clifford(self, num_qubits): @combine(num_qubits=[4, 5, 6, 7]) def test_decompose_lnn_depth(self, num_qubits): - """Test layered decomposition for linear-nearest-neighbour (LNN) connectivity.""" + """Test layered decomposition for linear-nearest-neighbor (LNN) connectivity.""" rng = np.random.default_rng(1234) samples = 10 for _ in range(samples): @@ -64,7 +64,7 @@ def test_decompose_lnn_depth(self, num_qubits): filter_function=lambda x: x.operation.num_qubits == 2 ) self.assertTrue(depth2q <= 7 * num_qubits + 2) - # Check that the Clifford circuit has linear nearest neighbour connectivity + # Check that the Clifford circuit has linear nearest neighbor connectivity self.assertTrue(check_lnn_connectivity(circ.decompose())) cliff_target = Clifford(circ) self.assertEqual(cliff, cliff_target) diff --git a/test/python/synthesis/test_cx_cz_synthesis.py b/test/python/synthesis/test_cx_cz_synthesis.py index ef7eeb38b8f..63353dab95d 100644 --- a/test/python/synthesis/test_cx_cz_synthesis.py +++ b/test/python/synthesis/test_cx_cz_synthesis.py @@ -34,7 +34,7 @@ class TestCXCZSynth(QiskitTestCase): @combine(num_qubits=[3, 4, 5, 6, 7, 8, 9, 10]) def test_cx_cz_synth_lnn(self, num_qubits): - """Test the CXCZ synthesis code for linear nearest neighbour connectivity.""" + """Test the CXCZ synthesis code for linear nearest neighbor connectivity.""" seed = 1234 rng = np.random.default_rng(seed) num_gates = 10 diff --git a/test/python/synthesis/test_cz_synthesis.py b/test/python/synthesis/test_cz_synthesis.py index 7284039e56c..af663a4f0d3 100644 --- a/test/python/synthesis/test_cz_synthesis.py +++ b/test/python/synthesis/test_cz_synthesis.py @@ -31,7 +31,7 @@ class TestCZSynth(QiskitTestCase): @combine(num_qubits=[3, 4, 5, 6, 7]) def test_cz_synth_lnn(self, num_qubits): - """Test the CZ synthesis code for linear nearest neighbour connectivity.""" + """Test the CZ synthesis code for linear nearest neighbor connectivity.""" seed = 1234 rng = np.random.default_rng(seed) num_gates = 10 diff --git a/test/python/synthesis/test_stabilizer_synthesis.py b/test/python/synthesis/test_stabilizer_synthesis.py index e195c9cf270..958faa204c1 100644 --- a/test/python/synthesis/test_stabilizer_synthesis.py +++ b/test/python/synthesis/test_stabilizer_synthesis.py @@ -54,7 +54,7 @@ def test_decompose_stab(self, num_qubits): @combine(num_qubits=[4, 5, 6, 7]) def test_decompose_lnn_depth(self, num_qubits): - """Test stabilizer state decomposition for linear-nearest-neighbour (LNN) connectivity.""" + """Test stabilizer state decomposition for linear-nearest-neighbor (LNN) connectivity.""" rng = np.random.default_rng(1234) samples = 10 for _ in range(samples): @@ -66,7 +66,7 @@ def test_decompose_lnn_depth(self, num_qubits): filter_function=lambda x: x.operation.num_qubits == 2 ) self.assertTrue(depth2q == 2 * num_qubits + 2) - # Check that the stabilizer state circuit has linear nearest neighbour connectivity + # Check that the stabilizer state circuit has linear nearest neighbor connectivity self.assertTrue(check_lnn_connectivity(circ.decompose())) stab_target = StabilizerState(circ) # Verify that the two stabilizers generate the same state diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index 8d953ff1b83..025b9accf22 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -219,7 +219,7 @@ def check_two_qubit_weyl_specialization( ): """Check that the two qubit Weyl decomposition gets specialized as expected""" - # Loop to check both for implicit and explicity specialization + # Loop to check both for implicit and explicitly specialization for decomposer in (TwoQubitWeylDecomposition, expected_specialization): if isinstance(decomposer, TwoQubitWeylDecomposition): with self.assertDebugOnly(): diff --git a/test/python/test_util.py b/test/python/test_util.py index f807f0d4e25..d403ed004bc 100644 --- a/test/python/test_util.py +++ b/test/python/test_util.py @@ -43,7 +43,7 @@ def test_local_hardware_no_cpu_count(self): self.assertEqual(1, result["cpus"]) def test_local_hardware_no_sched_five_count(self): - """Test cpu cound if sched affinity method is missing and cpu count is 5.""" + """Test cpu could if sched affinity method is missing and cpu count is 5.""" with mock.patch.object(multiprocessing, "os", spec=[]): multiprocessing.os.cpu_count = mock.MagicMock(return_value=5) del multiprocessing.os.sched_getaffinity @@ -51,7 +51,7 @@ def test_local_hardware_no_sched_five_count(self): self.assertEqual(2, result["cpus"]) def test_local_hardware_no_sched_sixty_four_count(self): - """Test cpu cound if sched affinity method is missing and cpu count is 64.""" + """Test cpu could if sched affinity method is missing and cpu count is 64.""" with mock.patch.object(multiprocessing, "os", spec=[]): multiprocessing.os.cpu_count = mock.MagicMock(return_value=64) del multiprocessing.os.sched_getaffinity diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index c1df8adbad2..c95d65422d4 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -406,7 +406,7 @@ def test_valid_pulse_duration(self): self.pulse_gate_validation_pass(circuit) def test_no_calibration(self): - """No error raises if no calibration is addedd.""" + """No error raises if no calibration is added.""" circuit = QuantumCircuit(1) circuit.x(0) diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 206a652299c..ff8be63ffbc 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -119,7 +119,7 @@ def test_can_combine_cliffords(self): cliff2 = self.create_cliff2() cliff3 = self.create_cliff3() - # Create a circuit with two consective cliffords + # Create a circuit with two consecutive cliffords qc1 = QuantumCircuit(4) qc1.append(cliff1, [3, 1, 2]) qc1.append(cliff2, [3, 1, 2]) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 1030b83ae2a..71bab61708c 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -198,7 +198,7 @@ def test_control_bit_of_cnot(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot1(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[Z]------.-- qr0:---[Z]--- | | @@ -219,7 +219,7 @@ def test_control_bit_of_cnot1(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot2(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[T]------.-- qr0:---[T]--- | | @@ -240,7 +240,7 @@ def test_control_bit_of_cnot2(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot3(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[Rz]------.-- qr0:---[Rz]--- | | @@ -261,7 +261,7 @@ def test_control_bit_of_cnot3(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot4(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[T]------.-- qr0:---[T]--- | | @@ -662,7 +662,7 @@ def test_basis_global_phase_02(self): self.assertEqual(Operator(circ), Operator(ccirc)) def test_basis_global_phase_03(self): - """Test global phase preservation if cummulative z-rotation is 0""" + """Test global phase preservation if cumulative z-rotation is 0""" circ = QuantumCircuit(1) circ.rz(np.pi / 2, 0) circ.p(np.pi / 2, 0) diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py index 9b34d095b3b..8a11af2bd68 100644 --- a/test/python/transpiler/test_consolidate_blocks.py +++ b/test/python/transpiler/test_consolidate_blocks.py @@ -517,7 +517,7 @@ def test_inverted_order(self): # The first two 'if' blocks here represent exactly the same operation as each other on the # outer bits, because in the second, the bit-order of the block is reversed, but so is the # order of the bits in the outer circuit that they're bound to, which makes them the same. - # The second two 'if' blocks also represnt the same operation as each other, but the 'first + # The second two 'if' blocks also represent the same operation as each other, but the 'first # two' and 'second two' pairs represent qubit-flipped operations. qc.if_test((0, False), body.copy(), qc.qubits, qc.clbits) qc.if_test((0, False), body.reverse_bits(), reversed(qc.qubits), qc.clbits) diff --git a/test/python/transpiler/test_full_ancilla_allocation.py b/test/python/transpiler/test_full_ancilla_allocation.py index 73d9708d0ba..452d9d93965 100644 --- a/test/python/transpiler/test_full_ancilla_allocation.py +++ b/test/python/transpiler/test_full_ancilla_allocation.py @@ -194,7 +194,7 @@ def test_name_collision(self): ) def test_bad_layout(self): - """Layout referes to a register that do not exist in the circuit""" + """Layout refers to a register that do not exist in the circuit""" qr = QuantumRegister(3, "q") circ = QuantumCircuit(qr) dag = circuit_to_dag(circ) diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py index 1e0f19b1a33..569a210f8a9 100644 --- a/test/python/transpiler/test_gate_direction.py +++ b/test/python/transpiler/test_gate_direction.py @@ -342,7 +342,7 @@ def test_symmetric_gates(self, gate): self.assertEqual(pass_(circuit), expected) def test_target_parameter_any(self): - """Test that a parametrised 2q gate is replaced correctly both if available and not + """Test that a parametrized 2q gate is replaced correctly both if available and not available.""" circuit = QuantumCircuit(2) circuit.rzx(1.5, 0, 1) @@ -356,7 +356,7 @@ def test_target_parameter_any(self): self.assertNotEqual(GateDirection(None, target=swapped)(circuit), circuit) def test_target_parameter_exact(self): - """Test that a parametrised 2q gate is detected correctly both if available and not + """Test that a parametrized 2q gate is detected correctly both if available and not available.""" circuit = QuantumCircuit(2) circuit.rzx(1.5, 0, 1) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index f20b102d183..ff54169374b 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -2045,7 +2045,7 @@ def test_unroll_empty_definition_with_phase(self): self.assertEqual(pass_(qc), expected) def test_leave_store_alone_basis(self): - """Don't attempt to synthesise `Store` instructions with basis gates.""" + """Don't attempt to synthesize `Store` instructions with basis gates.""" pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, basis_gates=["u", "cx"]) @@ -2068,7 +2068,7 @@ def test_leave_store_alone_basis(self): self.assertEqual(pass_(qc), expected) def test_leave_store_alone_with_target(self): - """Don't attempt to synthesise `Store` instructions with a `Target`.""" + """Don't attempt to synthesize `Store` instructions with a `Target`.""" # Note no store. target = Target() diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index bd14891bb8c..1431449779b 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -98,7 +98,7 @@ def test_valid_pulse_duration(self): pm.run(circuit) def test_no_calibration(self): - """No error raises if no calibration is addedd.""" + """No error raises if no calibration is added.""" circuit = QuantumCircuit(1) circuit.x(0) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index c00208a2ffa..ee85dc34ffd 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -1110,7 +1110,7 @@ def test_1(self, circuit, level): self.assertIn("swap", resulting_basis) # Skipping optimization level 3 because the swap gates get absorbed into - # a unitary block as part of the KAK decompostion optimization passes and + # a unitary block as part of the KAK decomposition optimization passes and # optimized away. @combine( level=[0, 1, 2], @@ -1487,7 +1487,7 @@ def _define(self): optimization_level=optimization_level, seed_transpiler=2022_10_04, ) - # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # Tests of the complete validity of a circuit are mostly done at the individual pass level; # here we're just checking that various passes do appear to have run. self.assertIsInstance(transpiled, QuantumCircuit) # Assert layout ran. diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 487fbf9daef..0a7b977162a 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -358,7 +358,7 @@ def test_dual_ghz_with_wide_barrier(self): self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) def test_dual_ghz_with_intermediate_barriers(self): - """Test dual ghz circuit with intermediate barriers local to each componennt.""" + """Test dual ghz circuit with intermediate barriers local to each component.""" qc = QuantumCircuit(8, name="double dhz") qc.h(0) qc.cz(0, 1) diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index fbe4e1fbf74..b1effdae7d8 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -241,7 +241,7 @@ def test_do_not_reorder_measurements(self): self.assertIsInstance(second_measure.operation, Measure) # Assert that the first measure is on the same qubit that the HGate was applied to, and the # second measurement is on a different qubit (though we don't care which exactly - that - # depends a little on the randomisation of the pass). + # depends a little on the randomization of the pass). self.assertEqual(last_h.qubits, first_measure.qubits) self.assertNotEqual(last_h.qubits, second_measure.qubits) diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 1e4da01cb42..d7c4baa18fd 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -43,7 +43,7 @@ def _ry_to_rz_template_pass(parameter: Parameter = None, extra_costs=None): - """Create a simple pass manager that runs a template optimisation with a single transformation. + """Create a simple pass manager that runs a template optimization with a single transformation. It turns ``RX(pi/2).RY(parameter).RX(-pi/2)`` into the equivalent virtual ``RZ`` rotation, where if ``parameter`` is given, it will be the instance used in the template.""" if parameter is None: @@ -409,7 +409,7 @@ def test_optimizer_does_not_replace_unbound_partial_match(self): circuit_out = PassManager(pass_).run(circuit_in) - # The template optimisation should not have replaced anything, because + # The template optimization should not have replaced anything, because # that would require it to leave dummy parameters in place without # binding them. self.assertEqual(circuit_in, circuit_out) diff --git a/test/python/transpiler/test_token_swapper.py b/test/python/transpiler/test_token_swapper.py index 9ded634eba7..8a3a8c72ee2 100644 --- a/test/python/transpiler/test_token_swapper.py +++ b/test/python/transpiler/test_token_swapper.py @@ -67,7 +67,7 @@ def test_small(self) -> None: self.assertEqual({i: i for i in range(8)}, permutation) def test_bug1(self) -> None: - """Tests for a bug that occured in happy swap chains of length >2.""" + """Tests for a bug that occurred in happy swap chains of length >2.""" graph = rx.PyGraph() graph.extend_from_edge_list( [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (3, 6)] diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index ceca591ce08..f6790e8ed14 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -71,7 +71,7 @@ class ControllableSynthesis(UnitarySynthesisPlugin): """A dummy synthesis plugin, which can have its ``supports_`` properties changed to test different parts of the synthesis plugin interface. By default, it accepts all keyword arguments and accepts all number of qubits, but if its run method is called, it just returns ``None`` to - indicate that the gate should not be synthesised.""" + indicate that the gate should not be synthesized.""" min_qubits = None max_qubits = None @@ -153,7 +153,7 @@ def mock_default_run_method(self): # We need to mock out DefaultUnitarySynthesis.run, except it will actually get called as an # instance method, so we can't just wrap the method defined on the class, but instead we # need to wrap a method that has been bound to a particular instance. This is slightly - # frgaile, because we're likely wrapping a _different_ instance, but since there are no + # fragile, because we're likely wrapping a _different_ instance, but since there are no # arguments to __init__, and no internal state, it should be ok. It doesn't matter if we # dodged the patching of the manager class that happens elsewhere in this test suite, # because we're always accessing something that the patch would delegate to the inner diff --git a/test/python/utils/test_lazy_loaders.py b/test/python/utils/test_lazy_loaders.py index bd63d7ff04a..11b37ccb9d1 100644 --- a/test/python/utils/test_lazy_loaders.py +++ b/test/python/utils/test_lazy_loaders.py @@ -423,7 +423,7 @@ def exec_module(self, module): def test_import_allows_attributes_failure(self): """Check that the import tester can accept a dictionary mapping module names to attributes, - and that these are recognised when they are missing.""" + and that these are recognized when they are missing.""" # We can just use existing modules for this. name_map = { "sys": ("executable", "path"), diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 2a0a61c7904..e7d28aac8a9 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -5626,7 +5626,7 @@ def test_draw_hamiltonian_single(self): self.assertEqual(circuit.draw(output="text").single_string(), expected) def test_draw_hamiltonian_multi(self): - """Text Hamiltonian gate with mutiple qubits.""" + """Text Hamiltonian gate with multiple qubits.""" expected = "\n".join( [ " ┌──────────────┐", @@ -5647,7 +5647,7 @@ def test_draw_hamiltonian_multi(self): class TestTextPhase(QiskitTestCase): - """Testing the draweing a circuit with phase""" + """Testing the drawing a circuit with phase""" def test_bell(self): """Text Bell state with phase.""" diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index f2ce2bee108..cc70cccf7d4 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -975,7 +975,7 @@ def load_qpy(qpy_files, version_parts): def _main(): - parser = argparse.ArgumentParser(description="Test QPY backwards compatibilty") + parser = argparse.ArgumentParser(description="Test QPY backwards compatibility") parser.add_argument("command", choices=["generate", "load"]) parser.add_argument( "--version", diff --git a/test/utils/_canonical.py b/test/utils/_canonical.py index 367281f512c..b05254b3e7b 100644 --- a/test/utils/_canonical.py +++ b/test/utils/_canonical.py @@ -52,7 +52,7 @@ def canonicalize_control_flow(circuit: QuantumCircuit) -> QuantumCircuit: """Canonicalize all control-flow operations in a circuit. This is not an efficient operation, and does not affect any properties of the circuit. Its - intent is to normalise parts of circuits that have a non-deterministic construction. These are + intent is to normalize parts of circuits that have a non-deterministic construction. These are the ordering of bit arguments in control-flow blocks output by the builder interface, and automatically generated ``for``-loop variables. From 591260f0694fb1944507283ce10cf8f068bd3935 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 20 Jun 2024 11:09:00 +0200 Subject: [PATCH 126/159] use compose instead of + (#12609) --- qiskit/circuit/library/n_local/two_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index 1cb388349fe..f3822d53243 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -110,7 +110,7 @@ class TwoLocal(NLocal): >>> entangler_map = [[0, 3], [0, 2]] # entangle the first and last two-way >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1) - >>> circuit = two + two + >>> circuit = two.compose(two) >>> print(circuit.decompose().draw()) # note, that the parameters are the same! q_0: ─────■───────────■───────────■───────────■────── │ │ │ │ From 7d1731b60d8dd6219a292dc62f24d1a8d780e43a Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 20 Jun 2024 22:01:56 +0900 Subject: [PATCH 127/159] Fix v2 pulse drawer (#12608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix error when V2 model is set * Apply suggestions from code review * Fix black --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Elena Peña Tapia --- qiskit/visualization/pulse_v2/device_info.py | 89 ++++++++++++------- .../fix-v2-pulse-drawer-d05e4e392766909f.yaml | 7 ++ 2 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml diff --git a/qiskit/visualization/pulse_v2/device_info.py b/qiskit/visualization/pulse_v2/device_info.py index 1e809c43abd..7898f978772 100644 --- a/qiskit/visualization/pulse_v2/device_info.py +++ b/qiskit/visualization/pulse_v2/device_info.py @@ -40,7 +40,7 @@ class :py:class:``DrawerBackendInfo`` with necessary methods to generate drawing from qiskit import pulse from qiskit.providers import BackendConfigurationError -from qiskit.providers.backend import Backend +from qiskit.providers.backend import Backend, BackendV2 class DrawerBackendInfo(ABC): @@ -106,40 +106,67 @@ def create_from_backend(cls, backend: Backend): Returns: OpenPulseBackendInfo: New configured instance. """ - configuration = backend.configuration() - defaults = backend.defaults() - - # load name - name = backend.name() - - # load cycle time - dt = configuration.dt - - # load frequencies chan_freqs = {} - - chan_freqs.update( - {pulse.DriveChannel(qind): freq for qind, freq in enumerate(defaults.qubit_freq_est)} - ) - chan_freqs.update( - {pulse.MeasureChannel(qind): freq for qind, freq in enumerate(defaults.meas_freq_est)} - ) - for qind, u_lo_mappers in enumerate(configuration.u_channel_lo): - temp_val = 0.0 + 0.0j - for u_lo_mapper in u_lo_mappers: - temp_val += defaults.qubit_freq_est[u_lo_mapper.q] * u_lo_mapper.scale - chan_freqs[pulse.ControlChannel(qind)] = temp_val.real - - # load qubit channel mapping qubit_channel_map = defaultdict(list) - for qind in range(configuration.n_qubits): - qubit_channel_map[qind].append(configuration.drive(qubit=qind)) - qubit_channel_map[qind].append(configuration.measure(qubit=qind)) - for tind in range(configuration.n_qubits): + + if hasattr(backend, "configuration") and hasattr(backend, "defaults"): + configuration = backend.configuration() + defaults = backend.defaults() + + name = configuration.backend_name + dt = configuration.dt + + # load frequencies + chan_freqs.update( + { + pulse.DriveChannel(qind): freq + for qind, freq in enumerate(defaults.qubit_freq_est) + } + ) + chan_freqs.update( + { + pulse.MeasureChannel(qind): freq + for qind, freq in enumerate(defaults.meas_freq_est) + } + ) + for qind, u_lo_mappers in enumerate(configuration.u_channel_lo): + temp_val = 0.0 + 0.0j + for u_lo_mapper in u_lo_mappers: + temp_val += defaults.qubit_freq_est[u_lo_mapper.q] * u_lo_mapper.scale + chan_freqs[pulse.ControlChannel(qind)] = temp_val.real + + # load qubit channel mapping + for qind in range(configuration.n_qubits): + qubit_channel_map[qind].append(configuration.drive(qubit=qind)) + qubit_channel_map[qind].append(configuration.measure(qubit=qind)) + for tind in range(configuration.n_qubits): + try: + qubit_channel_map[qind].extend(configuration.control(qubits=(qind, tind))) + except BackendConfigurationError: + pass + elif isinstance(backend, BackendV2): + # Pure V2 model doesn't contain channel frequency information. + name = backend.name + dt = backend.dt + + # load qubit channel mapping + for qind in range(backend.num_qubits): + # channels are NotImplemented by default so we must catch arbitrary error. + try: + qubit_channel_map[qind].append(backend.drive_channel(qind)) + except Exception: # pylint: disable=broad-except + pass try: - qubit_channel_map[qind].extend(configuration.control(qubits=(qind, tind))) - except BackendConfigurationError: + qubit_channel_map[qind].append(backend.measure_channel(qind)) + except Exception: # pylint: disable=broad-except pass + for tind in range(backend.num_qubits): + try: + qubit_channel_map[qind].extend(backend.control_channel(qubits=(qind, tind))) + except Exception: # pylint: disable=broad-except + pass + else: + raise RuntimeError("Backend object not yet supported") return OpenPulseBackendInfo( name=name, dt=dt, channel_frequency_map=chan_freqs, qubit_channel_map=qubit_channel_map diff --git a/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml b/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml new file mode 100644 index 00000000000..b158703c6b8 --- /dev/null +++ b/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in :func:`qiskit.visualization.pulse_v2.interface.draw` that didn't + draw pulse schedules when the draw function was called with a :class:`.BackendV2` argument. + Because the V2 backend doesn't report hardware channel frequencies, + the generated drawing will show 'no freq.' below each channel label. From b6c61661272c2e242963c416e64a9a2e050ea25d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Jun 2024 18:15:25 +0100 Subject: [PATCH 128/159] Invalidate `parameters` cache on circuit copy (#12619) Previously, the caching of the parameter view could persist between copies of the circuit because it was part of the `copy.copy`. --- qiskit/circuit/quantumcircuit.py | 2 ++ .../fix-parameter-cache-05eac2f24477ccb8.yaml | 7 ++++++ test/python/circuit/test_parameters.py | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index a88dfd43ea4..ee52e3308a9 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3674,6 +3674,8 @@ 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) diff --git a/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml new file mode 100644 index 00000000000..05ac759569f --- /dev/null +++ b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The :attr:`.QuantumCircuit.parameters` attribute will now correctly be empty + when using :meth:`.QuantumCircuit.copy_empty_like` on a parametric circuit. + Previously, an internal cache would be copied over without invalidation. + Fix `#12617 `__. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 0095f87be9a..f841f969c00 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -203,6 +203,29 @@ def test_parameters_property_by_index(self): for i, vi in enumerate(v): self.assertEqual(vi, qc.parameters[i]) + def test_parameters_property_independent_after_copy(self): + """Test that any `parameters` property caching is invalidated after a copy operation.""" + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + + qc1 = QuantumCircuit(1) + qc1.rz(a, 0) + self.assertEqual(set(qc1.parameters), {a}) + + qc2 = qc1.copy_empty_like() + self.assertEqual(set(qc2.parameters), set()) + + qc3 = qc1.copy() + self.assertEqual(set(qc3.parameters), {a}) + qc3.rz(b, 0) + self.assertEqual(set(qc3.parameters), {a, b}) + self.assertEqual(set(qc1.parameters), {a}) + + qc1.rz(c, 0) + self.assertEqual(set(qc1.parameters), {a, c}) + self.assertEqual(set(qc3.parameters), {a, b}) + def test_get_parameter(self): """Test the `get_parameter` method.""" x = Parameter("x") From b8de17f9082089275ae979c21cf03b8e99e245b3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Jun 2024 11:15:07 +0100 Subject: [PATCH 129/159] Localise `py_op` caching data in `RefCell` (#12594) This localises the caching concerns of the `PackedInstruction::py_op` field into a method `unpack_py_op`, which can now update the cache through an immutable reference (if no other immutable references are taken out). Having the new method to encapsulate the `cache_pyops` feature simplifies the large `#[cfg(feature = "cache_pyop")]` gates. --- crates/circuit/src/circuit_data.rs | 363 ++-------------------- crates/circuit/src/circuit_instruction.rs | 82 ++++- 2 files changed, 102 insertions(+), 343 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index da35787e320..07f4579a4cd 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,10 +10,13 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; + use crate::bit_data::BitData; use crate::circuit_instruction::{ - convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, - ExtraInstructionAttributes, OperationInput, PackedInstruction, + convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, + PackedInstruction, }; use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; @@ -489,66 +492,40 @@ impl CircuitData { .getattr(intern!(py, "deepcopy"))?; for inst in &mut res.data { match &mut inst.op { - OperationType::Standard(_) => { - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } - } + OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { op.gate = deepcopy.call1((&op.gate,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Instruction(ref mut op) => { op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Operation(ref mut op) => { op.operation = deepcopy.call1((&op.operation,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } } } else if copy_instructions { for inst in &mut res.data { match &mut inst.op { - OperationType::Standard(_) => { - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } - } + OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Instruction(ref mut op) => { op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Operation(ref mut op) => { op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } } } Ok(res) @@ -589,87 +566,10 @@ impl CircuitData { /// Args: /// func (Callable[[:class:`~.Operation`], None]): /// The callable to invoke. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - - let op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - func.call1((op,))?; - } - Ok(()) - } - - /// Invokes callable ``func`` with each instruction's operation. - /// - /// Args: - /// func (Callable[[:class:`~.Operation`], None]): - /// The callable to invoke. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn foreach_op(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for inst in self.data.iter_mut() { - let op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } - }; - func.call1((op,))?; + func.call1((inst.unpack_py_op(py)?,))?; } Ok(()) } @@ -680,88 +580,10 @@ impl CircuitData { /// Args: /// func (Callable[[int, :class:`~.Operation`], None]): /// The callable to invoke. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - - let op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - func.call1((index, op))?; - } - Ok(()) - } - - /// Invokes callable ``func`` with the positional index and operation - /// of each instruction. - /// - /// Args: - /// func (Callable[[int, :class:`~.Operation`], None]): - /// The callable to invoke. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn foreach_op_indexed(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for (index, inst) in self.data.iter_mut().enumerate() { - let op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } - }; - func.call1((index, op))?; + func.call1((index, inst.unpack_py_op(py)?))?; } Ok(()) } @@ -779,49 +601,23 @@ impl CircuitData { /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their /// replacements. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - let old_op = match &inst.op { - OperationType::Standard(op) => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - if condition.is_some() { - operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )? - } else { - op.into_py(py) + let py_op = { + if let OperationType::Standard(op) = inst.op { + match inst.extra_attrs.as_deref() { + None + | Some(ExtraInstructionAttributes { + condition: None, .. + }) => op.into_py(py), + _ => inst.unpack_py_op(py)?, } + } else { + inst.unpack_py_op(py)? } - OperationType::Gate(op) => op.gate.clone_ref(py), - OperationType::Instruction(op) => op.instruction.clone_ref(py), - OperationType::Operation(op) => op.operation.clone_ref(py), }; - let result: OperationInput = func.call1((old_op,))?.extract()?; + let result: OperationInput = func.call1((py_op,))?.extract()?; match result { OperationInput::Standard(op) => { inst.op = OperationType::Standard(op); @@ -836,7 +632,7 @@ impl CircuitData { inst.op = OperationType::Operation(op); } OperationInput::Object(new_op) => { - let new_inst_details = convert_py_to_operation_type(py, new_op)?; + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; inst.op = new_inst_details.operation; inst.params = new_inst_details.params; if new_inst_details.label.is_some() @@ -851,103 +647,10 @@ impl CircuitData { condition: new_inst_details.condition, })) } - } - } - } - Ok(()) - } - - /// Invokes callable ``func`` with each instruction's operation, - /// replacing the operation with the result. - /// - /// .. note:: - /// - /// This is only to be used by map_vars() in quantumcircuit.py it - /// assumes that a full Python instruction will only be returned from - /// standard gates iff a condition is set. - /// - /// Args: - /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): - /// A callable used to map original operation to their - /// replacements. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for inst in self.data.iter_mut() { - let old_op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => match &inst.op { - OperationType::Standard(op) => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - if condition.is_some() { - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } else { - op.into_py(py) - } - } - OperationType::Gate(op) => op.gate.clone_ref(py), - OperationType::Instruction(op) => op.instruction.clone_ref(py), - OperationType::Operation(op) => op.operation.clone_ref(py), - }, - }; - let result: OperationInput = func.call1((old_op,))?.extract()?; - match result { - OperationInput::Standard(op) => { - inst.op = OperationType::Standard(op); - } - OperationInput::Gate(op) => { - inst.op = OperationType::Gate(op); - } - OperationInput::Instruction(op) => { - inst.op = OperationType::Instruction(op); - } - OperationInput::Operation(op) => { - inst.op = OperationType::Operation(op); - } - OperationInput::Object(new_op) => { - let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; - inst.op = new_inst_details.operation; - inst.params = new_inst_details.params; - if new_inst_details.label.is_some() - || new_inst_details.duration.is_some() - || new_inst_details.unit.is_some() - || new_inst_details.condition.is_some() + #[cfg(feature = "cache_pygates")] { - inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { - label: new_inst_details.label, - duration: new_inst_details.duration, - unit: new_inst_details.unit, - condition: new_inst_details.condition, - })) + *inst.py_op.borrow_mut() = Some(new_op); } - inst.py_op = Some(new_op); } } } @@ -1537,7 +1240,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: inst.py_op.clone(), + py_op: RefCell::new(inst.py_op.clone()), }) } @@ -1557,7 +1260,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: inst.py_op.clone(), + py_op: RefCell::new(inst.py_op.clone()), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 2bb90367082..5179190d8aa 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -10,6 +10,9 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; + use pyo3::basic::CompareOp; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -47,8 +50,61 @@ pub(crate) struct PackedInstruction { pub clbits_id: Index, pub params: SmallVec<[Param; 3]>, pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] - pub py_op: Option, + /// This is hidden in a `RefCell` because, while that has additional memory-usage implications + /// while we're still building with the feature enabled, we intend to remove the feature in the + /// future, and hiding the cache within a `RefCell` lets us keep the cache transparently in our + /// interfaces, without needing various functions to unnecessarily take `&mut` references. + pub py_op: RefCell>, +} + +impl PackedInstruction { + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this + /// instruction. This may construct the reference if the `PackedInstruction` is a standard + /// gate with no already stored operation. + /// + /// A standard-gate operation object returned by this function is disconnected from the + /// containing circuit; updates to its label, duration, unit and condition will not be + /// propagated back. + pub fn unpack_py_op(&self, py: Python) -> PyResult> { + #[cfg(feature = "cache_pygates")] + { + if let Some(cached_op) = self.py_op.borrow().as_ref() { + return Ok(cached_op.clone_ref(py)); + } + } + let (label, duration, unit, condition) = match self.extra_attrs.as_deref() { + Some(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + }) => ( + label.as_deref(), + duration.as_ref(), + unit.as_deref(), + condition.as_ref(), + ), + None => (None, None, None, None), + }; + let out = operation_type_and_data_to_py( + py, + &self.op, + &self.params, + label, + duration, + unit, + condition, + )?; + #[cfg(feature = "cache_pygates")] + { + if let Ok(mut cell) = self.py_op.try_borrow_mut() { + cell.get_or_insert_with(|| out.clone_ref(py)); + } + } + Ok(out) + } } /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and @@ -666,20 +722,20 @@ pub(crate) fn operation_type_to_py( let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { None => (None, None, None, None), Some(extra_attrs) => ( - extra_attrs.label.clone(), - extra_attrs.duration.clone(), - extra_attrs.unit.clone(), - extra_attrs.condition.clone(), + extra_attrs.label.as_deref(), + extra_attrs.duration.as_ref(), + extra_attrs.unit.as_deref(), + extra_attrs.condition.as_ref(), ), }; operation_type_and_data_to_py( py, &circuit_inst.operation, &circuit_inst.params, - &label, - &duration, - &unit, - &condition, + label, + duration, + unit, + condition, ) } @@ -692,10 +748,10 @@ pub(crate) fn operation_type_and_data_to_py( py: Python, operation: &OperationType, params: &[Param], - label: &Option, - duration: &Option, - unit: &Option, - condition: &Option, + label: Option<&str>, + duration: Option<&PyObject>, + unit: Option<&str>, + condition: Option<&PyObject>, ) -> PyResult { match &operation { OperationType::Standard(op) => { From 91f0c70885e6b58d1168e43c330373197b1c19ee Mon Sep 17 00:00:00 2001 From: "Tiago R. Cunha" <155388148+cstiago@users.noreply.github.com> Date: Fri, 21 Jun 2024 08:29:48 -0300 Subject: [PATCH 130/159] Fix type hint in SolovayKitaevDecomposition (#12627) The return type hint of `find_basic_approximation` method changes from `Gate` to `GateSequence` in `SolovayKitaevDecomposition` class, as implied by `self.basic_approximations`. With no remaining mentions of `Gate`, its corresponding import statement is removed. --- qiskit/synthesis/discrete_basis/solovay_kitaev.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 2e5cfeafcec..2c8df5bd1b6 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -16,8 +16,6 @@ import numpy as np -from qiskit.circuit.gate import Gate - from .gate_sequence import GateSequence from .commutator_decompose import commutator_decompose from .generate_basis_approximations import generate_basic_approximations, _1q_gates, _1q_inverses @@ -157,14 +155,14 @@ def _recurse(self, sequence: GateSequence, n: int, check_input: bool = True) -> w_n1 = self._recurse(w_n, n - 1, check_input=check_input) return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1) - def find_basic_approximation(self, sequence: GateSequence) -> Gate: - """Finds gate in ``self._basic_approximations`` that best represents ``sequence``. + def find_basic_approximation(self, sequence: GateSequence) -> GateSequence: + """Find ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. Args: - sequence: The gate to find the approximation to. + sequence: ``GateSequence`` to find the approximation to. Returns: - Gate in basic approximations that is closest to ``sequence``. + ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. """ # TODO explore using a k-d tree here From 8752f900f090bb46bc1e2468d76f99a797b9182f Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Fri, 21 Jun 2024 08:18:28 -0400 Subject: [PATCH 131/159] Add remaining tests for `ParameterVector` and tweak its `repr` (#12597) --- qiskit/circuit/parametervector.py | 2 +- test/python/circuit/test_parameters.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py index 151e3e7fea7..7b32395e143 100644 --- a/qiskit/circuit/parametervector.py +++ b/qiskit/circuit/parametervector.py @@ -87,7 +87,7 @@ def __str__(self): return f"{self.name}, {[str(item) for item in self.params]}" def __repr__(self): - return f"{self.__class__.__name__}(name={self.name}, length={len(self)})" + return f"{self.__class__.__name__}(name={repr(self.name)}, length={len(self)})" def resize(self, length): """Resize the parameter vector. If necessary, new elements are generated. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index f841f969c00..f580416eccf 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1398,6 +1398,21 @@ def test_parametervector_resize(self): self.assertEqual(element, vec[1]) self.assertListEqual([param.name for param in vec], _paramvec_names("x", 3)) + def test_parametervector_repr(self): + """Test the __repr__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(repr(vec), "ParameterVector(name='x', length=2)") + + def test_parametervector_str(self): + """Test the __str__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(str(vec), "x, ['x[0]', 'x[1]']") + + def test_parametervector_index(self): + """Test the index method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(vec.index(vec[1]), 1) + def test_raise_if_sub_unknown_parameters(self): """Verify we raise if asked to sub a parameter not in self.""" x = Parameter("x") From 22c145aa0e978e6cdefd59d0ff43e371b2d6487b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Jun 2024 13:28:57 +0100 Subject: [PATCH 132/159] Fix `CircuitInstruction` legacy iterable typing (#12630) The legacy 3-tuple format of `CircuitInstruction` still exposes the object in the old tuple-like way of `(Operation, list[Qubit], list[Clbit])`, rather than the new-style attribute access using tuples for the qargs and cargs. This was inadvertantly changed when it moved to Rust. --- crates/circuit/src/circuit_instruction.rs | 12 ++++++++++-- test/python/circuit/test_circuit_data.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 5179190d8aa..93e73ccbc42 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -542,7 +542,11 @@ impl CircuitInstruction { Ok(PyTuple::new_bound( py, - [op, self.qubits.to_object(py), self.clbits.to_object(py)], + [ + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), + ], )) } @@ -558,7 +562,11 @@ impl CircuitInstruction { }; Ok(PyTuple::new_bound( py, - [op, self.qubits.to_object(py), self.clbits.to_object(py)], + [ + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), + ], )) } diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 6fc6e8e72bd..35ae27b2fcf 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -403,6 +403,22 @@ class TestQuantumCircuitInstructionData(QiskitTestCase): # but are included as tests to maintain compatability with the previous # list interface of circuit.data. + def test_iteration_of_data_entry(self): + """Verify that the base types of the legacy tuple iteration are correct, since they're + different to attribute access.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.measure([0, 1, 2], [0, 1, 2]) + + def to_legacy(instruction): + return (instruction.operation, list(instruction.qubits), list(instruction.clbits)) + + expected = [to_legacy(instruction) for instruction in qc.data] + actual = [tuple(instruction) for instruction in qc.data] + self.assertEqual(actual, expected) + def test_getitem_by_insertion_order(self): """Verify one can get circuit.data items in insertion order.""" qr = QuantumRegister(2) From 87aa89c19387ef7ff4177ef5144f60119da392e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:25:59 +0200 Subject: [PATCH 133/159] Add Rust representation for `XXMinusYYGate` and `XXPlusYYGate` (#12606) * Add XXMinusYYGate and XXPlusYYGate, implement parameter multiplication function (naive approach). Co-authored by: Julien Gacon jul@zurich.ibm.com * * Format code * Use multiply_param in RZGate * Fix signs and indices --- crates/circuit/src/gate_matrix.rs | 46 +++++ crates/circuit/src/imports.rs | 14 +- crates/circuit/src/operations.rs | 167 ++++++++++++++---- .../library/standard_gates/xx_minus_yy.py | 3 + .../library/standard_gates/xx_plus_yy.py | 3 + 5 files changed, 194 insertions(+), 39 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index ad8c918e73b..23ce9486922 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -212,6 +212,7 @@ pub static ISWAP_GATE: [[Complex64; 4]; 4] = [ ]; pub static S_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., 1.)]]; + pub static SDG_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., -1.)]]; @@ -219,6 +220,7 @@ pub static T_GATE: [[Complex64; 2]; 2] = [ [c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)], ]; + pub static TDG_GATE: [[Complex64; 2]; 2] = [ [c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], @@ -246,3 +248,47 @@ pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], ] } + +#[inline] +pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [ + c64(cos, 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., -sin) * c64(0., -beta).exp(), + ], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [ + c64(0., -sin) * c64(0., beta).exp(), + c64(0., 0.), + c64(0., 0.), + c64(cos, 0.), + ], + ] +} + +#[inline] +pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [ + c64(0., 0.), + c64(cos, 0.), + c64(0., -sin) * c64(0., -beta).exp(), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., -sin) * c64(0., beta).exp(), + c64(cos, 0.), + c64(0., 0.), + ], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 8db3b88fd7d..3a9a942db8d 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -124,13 +124,23 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // SdgGate = 19 ["qiskit.circuit.library.standard_gates.s", "SdgGate"], // TGate = 20 - ["qiskit.circuit.library.standard_gates.s", "TGate"], + ["qiskit.circuit.library.standard_gates.t", "TGate"], // TdgGate = 21 - ["qiskit.circuit.library.standard_gates.s", "TdgGate"], + ["qiskit.circuit.library.standard_gates.t", "TdgGate"], // SXdgGate = 22 ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], // iSWAPGate = 23 ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], + //XXMinusYYGate = 24 + [ + "qiskit.circuit.library.standard_gates.xx_minus_yy", + "XXMinusYYGate", + ], + //XXPlusYYGate = 25 + [ + "qiskit.circuit.library.standard_gates.xx_plus_yy", + "XXPlusYYGate", + ], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 9048c55d9d4..6dedd3ac206 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -203,14 +203,16 @@ pub enum StandardGate { TdgGate = 21, SXdgGate = 22, ISwapGate = 23, + XXMinusYYGate = 24, + XXPlusYYGate = 25, } static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ - 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, ]; static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -238,6 +240,8 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "tdg", "sxdg", "iswap", + "xx_minus_yy", + "xx_plus_yy", ]; #[pymethods] @@ -287,7 +291,7 @@ impl StandardGate { // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 24; +pub const STANDARD_GATE_SIZE: usize = 26; impl Operation for StandardGate { fn name(&self) -> &str { @@ -416,6 +420,18 @@ impl Operation for StandardGate { [] => Some(aview2(&gate_matrix::ISWAP_GATE).to_owned()), _ => None, }, + Self::XXMinusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_minus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, + Self::XXPlusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_plus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, } } @@ -502,6 +518,7 @@ impl Operation for StandardGate { }), Self::CXGate => None, Self::CCXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; let q1 = smallvec![Qubit(1)]; let q2 = smallvec![Qubit(2)]; let q0_1 = smallvec![Qubit(0), Qubit(1)]; @@ -524,7 +541,7 @@ impl Operation for StandardGate { (Self::TGate, smallvec![], q2.clone()), (Self::HGate, smallvec![], q2), (Self::CXGate, smallvec![], q0_1.clone()), - (Self::TGate, smallvec![], smallvec![Qubit(0)]), + (Self::TGate, smallvec![], q0), (Self::TdgGate, smallvec![], q1), (Self::CXGate, smallvec![], q0_1), ], @@ -536,39 +553,20 @@ impl Operation for StandardGate { Self::RXGate => todo!("Add when we have R"), Self::RYGate => todo!("Add when we have R"), Self::RZGate => Python::with_gil(|py| -> Option { - match ¶ms[0] { - Param::Float(theta) => Some( - CircuitData::from_standard_gates( - py, - 1, - [( - Self::PhaseGate, - smallvec![Param::Float(*theta)], - smallvec![Qubit(0)], - )], - Param::Float(-0.5 * theta), - ) - .expect("Unexpected Qiskit python bug"), - ), - Param::ParameterExpression(theta) => Some( - CircuitData::from_standard_gates( - py, - 1, - [( - Self::PhaseGate, - smallvec![Param::ParameterExpression(theta.clone_ref(py))], - smallvec![Qubit(0)], - )], - Param::ParameterExpression( - theta - .call_method1(py, intern!(py, "__rmul__"), (-0.5,)) - .expect("Parameter expression for global phase failed"), - ), - ) - .expect("Unexpected Qiskit python bug"), - ), - Param::Obj(_) => unreachable!(), - } + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![theta.clone()], + smallvec![Qubit(0)], + )], + multiply_param(theta, -0.5, py), + ) + .expect("Unexpected Qiskit python bug"), + ) }), Self::ECRGate => todo!("Add when we have RZX"), Self::SwapGate => Python::with_gil(|py| -> Option { @@ -732,6 +730,88 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::XXMinusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RZGate, + smallvec![multiply_param(beta, -1.0, py)], + q1.clone(), + ), + (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::SXGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q0.clone()), + (Self::SGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, 0.5, py)], + q0.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::SdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::SXdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q0), + (Self::RZGate, smallvec![beta.clone()], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XXPlusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q1_0 = smallvec![Qubit(1), Qubit(0)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RZGate, smallvec![beta.clone()], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::SXGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q1.clone()), + (Self::SGate, smallvec![], q0.clone()), + (Self::CXGate, smallvec![], q1_0.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q0.clone(), + ), + (Self::CXGate, smallvec![], q1_0), + (Self::SdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::SXdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q1), + (Self::RZGate, smallvec![multiply_param(beta, -1.0, py)], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), } } @@ -742,6 +822,19 @@ impl Operation for StandardGate { const FLOAT_ZERO: Param = Param::Float(0.0); +fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta * mult), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__rmul__"), (mult,)) + .expect("Parameter expression for global phase failed"), + ), + Param::Obj(_) => unreachable!(), + } +} + /// This class is used to wrap a Python side Instruction that is not in the standard library #[derive(Clone, Debug)] #[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index 4bf4ab80eca..db3c3dc8915 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -27,6 +27,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class XXMinusYYGate(Gate): @@ -91,6 +92,8 @@ class XXMinusYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.XXMinusYYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index a82316ed7b0..7920454d0b9 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class XXPlusYYGate(Gate): @@ -87,6 +88,8 @@ class XXPlusYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.XXPlusYYGate + def __init__( self, theta: ParameterValueType, From de6c6eb2f944c58ecf2258801e921e0846d41d89 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 24 Jun 2024 10:02:31 +0200 Subject: [PATCH 134/159] Follow up on #12327: circuit construction in Rust (#12605) * Follow up on #12327 also port circuit construction to rust and add a reno * move _get_ordered_swap to Rust only * drop redundant Ok(expect()) * proper synthesis structure --- crates/accelerate/src/lib.rs | 2 +- crates/accelerate/src/synthesis/mod.rs | 22 ++++++ .../src/synthesis/permutation/mod.rs | 68 +++++++++++++++++++ .../permutation/utils.rs} | 58 ++++------------ crates/pyext/src/lib.rs | 10 +-- qiskit/__init__.py | 2 +- .../library/generalized_gates/permutation.py | 17 +++-- .../synthesis/permutation/permutation_full.py | 14 +--- .../permutation/permutation_utils.py | 3 +- .../oxidize-permbasic-be27578187ac472f.yaml | 4 ++ .../synthesis/test_permutation_synthesis.py | 14 ---- 11 files changed, 124 insertions(+), 90 deletions(-) create mode 100644 crates/accelerate/src/synthesis/mod.rs create mode 100644 crates/accelerate/src/synthesis/permutation/mod.rs rename crates/accelerate/src/{permutation.rs => synthesis/permutation/utils.rs} (66%) create mode 100644 releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 3924c1de409..dcfbdc9f187 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -23,12 +23,12 @@ pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; -pub mod permutation; pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; pub mod stochastic_swap; +pub mod synthesis; pub mod two_qubit_decompose; pub mod uc_gate; pub mod utils; diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs new file mode 100644 index 00000000000..f1a72045921 --- /dev/null +++ b/crates/accelerate/src/synthesis/mod.rs @@ -0,0 +1,22 @@ +// 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. + +mod permutation; + +use pyo3::prelude::*; +use pyo3::wrap_pymodule; + +#[pymodule] +pub fn synthesis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs new file mode 100644 index 00000000000..bf0ff97848f --- /dev/null +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -0,0 +1,68 @@ +// 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 numpy::PyArrayLike1; +use smallvec::smallvec; + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; + +mod utils; + +/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + utils::validate_permutation(&view)?; + Ok(py.None()) +} + +/// Finds inverse of a permutation pattern. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let inverse_i64: Vec = utils::invert(&view).iter().map(|&x| x as i64).collect(); + Ok(inverse_i64.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _synth_permutation_basic(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let num_qubits = view.len(); + CircuitData::from_standard_gates( + py, + num_qubits as u32, + utils::get_ordered_swap(&view).iter().map(|(i, j)| { + ( + StandardGate::SwapGate, + smallvec![], + smallvec![Qubit(*i as u32), Qubit(*j as u32)], + ) + }), + Param::Float(0.0), + ) +} + +#[pymodule] +pub fn permutation(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?; + m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?; + m.add_function(wrap_pyfunction!(_synth_permutation_basic, m)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/permutation.rs b/crates/accelerate/src/synthesis/permutation/utils.rs similarity index 66% rename from crates/accelerate/src/permutation.rs rename to crates/accelerate/src/synthesis/permutation/utils.rs index 31ba433ddd3..a78088bfbfa 100644 --- a/crates/accelerate/src/permutation.rs +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -11,12 +11,11 @@ // that they have been altered from the originals. use ndarray::{Array1, ArrayView1}; -use numpy::PyArrayLike1; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use std::vec::Vec; -fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { +pub fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { let n = pattern.len(); let mut seen: Vec = vec![false; n]; @@ -47,7 +46,7 @@ fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { Ok(()) } -fn invert(pattern: &ArrayView1) -> Array1 { +pub fn invert(pattern: &ArrayView1) -> Array1 { let mut inverse: Array1 = Array1::zeros(pattern.len()); pattern.iter().enumerate().for_each(|(ii, &jj)| { inverse[jj as usize] = ii; @@ -55,7 +54,16 @@ fn invert(pattern: &ArrayView1) -> Array1 { inverse } -fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { +/// Sorts the input permutation by iterating through the permutation list +/// and putting each element to its correct position via a SWAP (if it's not +/// at the correct position already). If ``n`` is the length of the input +/// permutation, this requires at most ``n`` SWAPs. +/// +/// More precisely, if the input permutation is a cycle of length ``m``, +/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); +/// if the input permutation consists of several disjoint cycles, then each cycle +/// is essentially treated independently. +pub fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { let mut permutation: Vec = pattern.iter().map(|&x| x as usize).collect(); let mut index_map = invert(pattern); @@ -76,45 +84,3 @@ fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { swaps[..].reverse(); swaps } - -/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. -#[pyfunction] -#[pyo3(signature = (pattern))] -fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { - let view = pattern.as_array(); - validate_permutation(&view)?; - Ok(py.None()) -} - -/// Finds inverse of a permutation pattern. -#[pyfunction] -#[pyo3(signature = (pattern))] -fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { - let view = pattern.as_array(); - let inverse_i64: Vec = invert(&view).iter().map(|&x| x as i64).collect(); - Ok(inverse_i64.to_object(py)) -} - -/// Sorts the input permutation by iterating through the permutation list -/// and putting each element to its correct position via a SWAP (if it's not -/// at the correct position already). If ``n`` is the length of the input -/// permutation, this requires at most ``n`` SWAPs. -/// -/// More precisely, if the input permutation is a cycle of length ``m``, -/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); -/// if the input permutation consists of several disjoint cycles, then each cycle -/// is essentially treated independently. -#[pyfunction] -#[pyo3(signature = (permutation_in))] -fn _get_ordered_swap(py: Python, permutation_in: PyArrayLike1) -> PyResult { - let view = permutation_in.as_array(); - Ok(get_ordered_swap(&view).to_object(py)) -} - -#[pymodule] -pub fn permutation(m: &Bound) -> PyResult<()> { - m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?; - m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?; - m.add_function(wrap_pyfunction!(_get_ordered_swap, m)?)?; - Ok(()) -} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index b80aad1a7a4..72f0d759099 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -17,10 +17,10 @@ use qiskit_accelerate::{ convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, 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, permutation::permutation, results::results, sabre::sabre, - sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, - stochastic_swap::stochastic_swap, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, - utils::utils, vf2_layout::vf2_layout, + 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, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, + vf2_layout::vf2_layout, }; #[pymodule] @@ -36,7 +36,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(nlayout))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(pauli_expval))?; - m.add_wrapped(wrap_pymodule!(permutation))?; + m.add_wrapped(wrap_pymodule!(synthesis))?; m.add_wrapped(wrap_pymodule!(results))?; m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index fce54433347..5b850565442 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -80,7 +80,7 @@ sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout -sys.modules["qiskit._accelerate.permutation"] = _accelerate.permutation +sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index 776c69d94f0..b2d17d2bed2 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -80,15 +80,13 @@ def __init__( name = "permutation_" + np.array_str(pattern).replace(" ", ",") - circuit = QuantumCircuit(num_qubits, name=name) - super().__init__(num_qubits, name=name) # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic - for i, j in _get_ordered_swap(pattern): - circuit.swap(i, j) + circuit = synth_permutation_basic(pattern) + circuit.name = name all_qubits = self.qubits self.append(circuit.to_gate(), all_qubits) @@ -184,10 +182,11 @@ def inverse(self, annotated: bool = False): def _qasm2_decomposition(self): # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_" - out = QuantumCircuit(self.num_qubits, name=name) - for i, j in _get_ordered_swap(self.pattern): - out.swap(i, j) + + out = synth_permutation_basic(self.pattern) + out.name = name + return out.to_gate() diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index ff014cb3a05..c280065c2a5 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -16,8 +16,8 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit._accelerate.synthesis.permutation import _synth_permutation_basic from .permutation_utils import ( - _get_ordered_swap, _inverse_pattern, _pattern_to_cycles, _decompose_cycles, @@ -44,17 +44,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc Returns: The synthesized quantum circuit. """ - # This is the very original Qiskit algorithm for synthesizing permutations. - - num_qubits = len(pattern) - qc = QuantumCircuit(num_qubits) - - swaps = _get_ordered_swap(pattern) - - for swap in swaps: - qc.swap(swap[0], swap[1]) - - return qc + return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern)) def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit: diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index dbd73bfe811..4520e18f4d0 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -13,9 +13,8 @@ """Utility functions for handling permutations.""" # pylint: disable=unused-import -from qiskit._accelerate.permutation import ( +from qiskit._accelerate.synthesis.permutation import ( _inverse_pattern, - _get_ordered_swap, _validate_permutation, ) diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml new file mode 100644 index 00000000000..e770aa1ca31 --- /dev/null +++ b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml @@ -0,0 +1,4 @@ +--- +upgrade_synthesis: + - | + Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust. diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 050df5a3fe1..b6a1ca9e185 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -27,7 +27,6 @@ ) from qiskit.synthesis.permutation.permutation_utils import ( _inverse_pattern, - _get_ordered_swap, _validate_permutation, ) from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -47,19 +46,6 @@ def test_inverse_pattern(self, width): for ii, jj in enumerate(pattern): self.assertTrue(inverse[jj] == ii) - @data(4, 5, 10, 15, 20) - def test_get_ordered_swap(self, width): - """Test _get_ordered_swap function produces correct swap list.""" - np.random.seed(1) - for _ in range(5): - pattern = np.random.permutation(width) - swap_list = _get_ordered_swap(pattern) - output = list(range(width)) - for i, j in swap_list: - output[i], output[j] = output[j], output[i] - self.assertTrue(np.array_equal(pattern, output)) - self.assertLess(len(swap_list), width) - @data(10, 20) def test_invalid_permutations(self, width): """Check that _validate_permutation raises exceptions when the From bf8f398fa4ddf287c6182b39bd27b324ab11dda0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 24 Jun 2024 10:05:06 -0400 Subject: [PATCH 135/159] Add rust representation for the u1, u2, and u3 gates (#12572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add rust representation for the u1, u2, and u3 gates This commit adds the rust representation of the U1Gate, U2Gate, and U3Gate to the `StandardGates` enum in rust. Part of #12566 * Update crates/circuit/src/imports.rs Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Fix test failures * Fix pylint pedantry --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/gate_matrix.rs | 32 +++++ crates/circuit/src/imports.rs | 6 + crates/circuit/src/operations.rs | 74 +++++++++++- qiskit/circuit/library/standard_gates/u1.py | 3 + qiskit/circuit/library/standard_gates/u2.py | 3 + qiskit/circuit/library/standard_gates/u3.py | 3 + test/python/circuit/test_rust_equivalence.py | 25 ++-- test/python/qasm3/test_export.py | 114 +++++++++--------- .../transpiler/test_optimize_1q_gates.py | 36 +++++- 9 files changed, 224 insertions(+), 72 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 23ce9486922..2e5f55d6ddc 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -271,6 +271,38 @@ pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { ] } +#[inline] +pub fn u1_gate(lam: f64) -> [[Complex64; 2]; 2] { + [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., lam).exp()], + ] +} + +#[inline] +pub fn u2_gate(phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + [ + [ + c64(FRAC_1_SQRT_2, 0.), + (-c64(0., lam).exp()) * FRAC_1_SQRT_2, + ], + [ + c64(0., phi).exp() * FRAC_1_SQRT_2, + c64(0., phi + lam).exp() * FRAC_1_SQRT_2, + ], + ] +} + +#[inline] +pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), -(c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} + #[inline] pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { let cos = (theta / 2.).cos(); diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 3a9a942db8d..7160798f56b 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -141,6 +141,12 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ "qiskit.circuit.library.standard_gates.xx_plus_yy", "XXPlusYYGate", ], + // U1Gate = 26 + ["qiskit.circuit.library.standard_gates.u1", "U1Gate"], + // U2Gate = 27 + ["qiskit.circuit.library.standard_gates.u2", "U2Gate"], + // U3Gate = 28 + ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 6dedd3ac206..451b0494738 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -205,14 +205,17 @@ pub enum StandardGate { ISwapGate = 23, XXMinusYYGate = 24, XXPlusYYGate = 25, + U1Gate = 26, + U2Gate = 27, + U3Gate = 28, } static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ - 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, ]; static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 3, ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -242,6 +245,9 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "iswap", "xx_minus_yy", "xx_plus_yy", + "u1", + "u2", + "u3", ]; #[pymethods] @@ -290,8 +296,7 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) - -pub const STANDARD_GATE_SIZE: usize = 26; +pub const STANDARD_GATE_SIZE: usize = 29; impl Operation for StandardGate { fn name(&self) -> &str { @@ -432,6 +437,22 @@ impl Operation for StandardGate { } _ => None, }, + Self::U1Gate => match params[0] { + Param::Float(val) => Some(aview2(&gate_matrix::u1_gate(val)).to_owned()), + _ => None, + }, + Self::U2Gate => match params { + [Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u2_gate(*phi, *lam)).to_owned()) + } + _ => None, + }, + Self::U3Gate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u3_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, } } @@ -667,6 +688,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U1Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::SdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( @@ -682,6 +718,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U2Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::TGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( @@ -697,6 +748,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U3Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::TdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index 1d59cabae1f..f141146b72d 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -19,6 +19,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int +from qiskit._accelerate.circuit import StandardGate class U1Gate(Gate): @@ -92,6 +93,8 @@ class U1Gate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.U1Gate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/u2.py b/qiskit/circuit/library/standard_gates/u2.py index c8e4de96efe..9e59cd4c5bb 100644 --- a/qiskit/circuit/library/standard_gates/u2.py +++ b/qiskit/circuit/library/standard_gates/u2.py @@ -18,6 +18,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U2Gate(Gate): @@ -86,6 +87,8 @@ class U2Gate(Gate): using two X90 pulses. """ + _standard_gate = StandardGate.U2Gate + def __init__( self, phi: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 0eef2518a85..f191609ea8f 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U3Gate(Gate): @@ -80,6 +81,8 @@ class U3Gate(Gate): U3(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.U3Gate + def __init__( self, theta: ParameterValueType, diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index bb09ae4caf3..8d6d159c0b6 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -79,12 +79,23 @@ def test_definitions(self): ) # Rust uses P but python still uses u1 elif rs_inst.operation.name == "p": - self.assertEqual(py_inst.operation.name, "u1") - self.assertEqual(rs_inst.operation.params, py_inst.operation.params) - self.assertEqual( - [py_def.find_bit(x).index for x in py_inst.qubits], - [rs_def.find_bit(x).index for x in rs_inst.qubits], - ) + if py_inst.operation.name == "u1": + self.assertEqual(py_inst.operation.name, "u1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, "u3") + self.assertEqual( + rs_inst.operation.params[0], py_inst.operation.params[2] + ) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: self.assertEqual(py_inst.operation.name, rs_inst.operation.name) self.assertEqual(rs_inst.operation.params, py_inst.operation.params) @@ -102,7 +113,7 @@ def test_matrix(self): continue with self.subTest(name=name): - params = [pi] * standard_gate._num_params() + params = [0.1] * standard_gate._num_params() py_def = gate_class.base_class(*params).to_matrix() rs_def = standard_gate._to_matrix(params) np.testing.assert_allclose(rs_def, py_def) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 048c5d7852b..6df04142088 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2018,65 +2018,63 @@ def test_teleportation(self): qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) - first_h = transpiled.data[0].operation - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - first_x = transpiled.data[-2].operation - u3_2 = first_x.definition.data[0].operation - first_z = transpiled.data[-1].operation - u1 = first_z.definition.data[0].operation - u3_3 = u1.definition.data[0].operation + id_len = len(str(id(transpiled.data[0].operation))) - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi) _gate_q_0;", - "}", - f"gate u1_{id(u1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_3)}(0, 0, pi) _gate_q_0;", - "}", - "gate z _gate_q_0 {", - f" u1_{id(u1)}(pi) _gate_q_0;", - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate z _gate_q_0 {", + re.compile(r" u1_\d{%s}\(pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "h $1;", + "cx $1, $2;", + "barrier $0, $1, $2;", + "cx $0, $1;", + "h $0;", + "barrier $0, $1, $2;", + "c[0] = measure $0;", + "c[1] = measure $1;", + "barrier $0, $1, $2;", + "if (c[1]) {", + " x $2;", + "}", + "if (c[0]) {", + " z $2;", + "}", + "", + ] + res = Exporter(includes=[]).dumps(transpiled).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" diff --git a/test/python/transpiler/test_optimize_1q_gates.py b/test/python/transpiler/test_optimize_1q_gates.py index 9253130bedb..e5483dd4749 100644 --- a/test/python/transpiler/test_optimize_1q_gates.py +++ b/test/python/transpiler/test_optimize_1q_gates.py @@ -19,7 +19,7 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator from qiskit.converters import circuit_to_dag -from qiskit.circuit import Parameter +from qiskit.circuit import Parameter, Gate from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, UGate, PhaseGate from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target @@ -323,9 +323,24 @@ def test_parameterized_expressions_in_circuits(self): def test_global_phase_u3_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate definition.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 @@ -337,9 +352,24 @@ def test_global_phase_u3_on_left(self): def test_global_phase_u_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 From 35f6297f20be6d9b58671948adc179dad77894af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:23:42 -0400 Subject: [PATCH 136/159] Bump faer from 0.19.0 to 0.19.1 (#12645) Bumps [faer](https://github.com/sarah-ek/faer-rs) from 0.19.0 to 0.19.1. - [Changelog](https://github.com/sarah-ek/faer-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sarah-ek/faer-rs/commits) --- updated-dependencies: - dependency-name: faer 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 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aefa3c932a0..454823748e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,9 +307,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ef9e1a4098a9e3a03c47bc5061406c04820552d869fd0fcd92587d07b271f0" +checksum = "41543c4de4bfb32efdffdd75cbcca5ef41b800e8a811ea4a41fb9393c6ef3bc0" dependencies = [ "bytemuck", "coe-rs", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index d9865d54543..b377a9b38a6 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" num-complex.workspace = true num-bigint = "0.4" rustworkx-core = "0.14" -faer = "0.19.0" +faer = "0.19.1" itertools = "0.13.0" qiskit-circuit.workspace = true From b20a7ceb58b0ec7e49004d3ce81bd3f2144e6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:16:16 +0200 Subject: [PATCH 137/159] Add placeholders for all mising standard gates in Rust (#12646) * Add placeholders for all gates, mark TODOs * Update name for CPhase * Remove todo from Ux gates --- crates/circuit/src/imports.rs | 53 ++++++++++- crates/circuit/src/operations.rs | 156 ++++++++++++++++++++++++------- 2 files changed, 175 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 7160798f56b..76e808d1b30 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -79,6 +79,7 @@ pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = /// /// NOTE: the order here is significant, the StandardGate variant's number must match /// index of it's entry in this table. This is all done statically for performance +// TODO: replace placeholders with actual implementation static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // ZGate = 0 ["qiskit.circuit.library.standard_gates.z", "ZGate"], @@ -131,12 +132,12 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], // iSWAPGate = 23 ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], - //XXMinusYYGate = 24 + // XXMinusYYGate = 24 [ "qiskit.circuit.library.standard_gates.xx_minus_yy", "XXMinusYYGate", ], - //XXPlusYYGate = 25 + // XXPlusYYGate = 25 [ "qiskit.circuit.library.standard_gates.xx_plus_yy", "XXPlusYYGate", @@ -147,6 +148,54 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ ["qiskit.circuit.library.standard_gates.u2", "U2Gate"], // U3Gate = 28 ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], + // CRXGate = 29 + ["placeholder", "placeholder"], + // CRYGate = 30 + ["placeholder", "placeholder"], + // CRZGate = 31 + ["placeholder", "placeholder"], + // RGate 32 + ["placeholder", "placeholder"], + // CHGate = 33 + ["qiskit.circuit.library.standard_gates.h", "CHGate"], + // CPhaseGate = 34 + ["qiskit.circuit.library.standard_gates.p", "CPhaseGate"], + // CSGate = 35 + ["qiskit.circuit.library.standard_gates.s", "CSGate"], + // CSdgGate = 36 + ["qiskit.circuit.library.standard_gates.s", "CSdgGate"], + // CSXGate = 37 + ["qiskit.circuit.library.standard_gates.sx", "CSXGate"], + // CSwapGate = 38 + ["qiskit.circuit.library.standard_gates.swap", "CSwapGate"], + // CUGate = 39 + ["qiskit.circuit.library.standard_gates.u", "CUGate"], + // CU1Gate = 40 + ["qiskit.circuit.library.standard_gates.u1", "CU1Gate"], + // CU3Gate = 41 + ["qiskit.circuit.library.standard_gates.u3", "CU3Gate"], + // C3XGate = 42 + ["placeholder", "placeholder"], + // C3SXGate = 43 + ["placeholder", "placeholder"], + // C4XGate = 44 + ["placeholder", "placeholder"], + // DCXGate = 45 + ["placeholder", "placeholder"], + // CCZGate = 46 + ["placeholder", "placeholder"], + // RCCXGate = 47 + ["placeholder", "placeholder"], + // RC3XGate = 48 + ["placeholder", "placeholder"], + // RXXGate = 49 + ["placeholder", "placeholder"], + // RYYGate = 50 + ["placeholder", "placeholder"], + // RZZGate = 51 + ["placeholder", "placeholder"], + // RZXGate = 52 + ["placeholder", "placeholder"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 451b0494738..af7dabc8621 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -208,46 +208,106 @@ pub enum StandardGate { U1Gate = 26, U2Gate = 27, U3Gate = 28, + CRXGate = 29, + CRYGate = 30, + CRZGate = 31, + RGate = 32, + CHGate = 33, + CPhaseGate = 34, + CSGate = 35, + CSdgGate = 36, + CSXGate = 37, + CSwapGate = 38, + CUGate = 39, + CU1Gate = 40, + CU3Gate = 41, + C3XGate = 42, + C3SXGate = 43, + C4XGate = 44, + DCXGate = 45, + CCZGate = 46, + RCCXGate = 47, + RC3XGate = 48, + RXXGate = 49, + RYYGate = 50, + RZZGate = 51, + RZXGate = 52, } +// TODO: replace all 34s (placeholders) with actual number static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ - 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 + 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 + 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 + 34, 34, 34, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 2, 2, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 34, 34, 34, // 50-52 ]; +// TODO: replace all 34s (placeholders) with actual number static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 3, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 + 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 + 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 + 34, 34, 34, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 1, 3, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 34, 34, 34, // 50-52 ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ - "z", - "y", - "x", - "cz", - "cy", - "cx", - "ccx", - "rx", - "ry", - "rz", - "ecr", - "swap", - "sx", - "global_phase", - "id", - "h", - "p", - "u", - "s", - "sdg", - "t", - "tdg", - "sxdg", - "iswap", - "xx_minus_yy", - "xx_plus_yy", - "u1", - "u2", - "u3", + "z", // 0 + "y", // 1 + "x", // 2 + "cz", // 3 + "cy", // 4 + "cx", // 5 + "ccx", // 6 + "rx", // 7 + "ry", // 8 + "rz", // 9 + "ecr", // 10 + "swap", // 11 + "sx", // 12 + "global_phase", // 13 + "id", // 14 + "h", // 15 + "p", // 16 + "u", // 17 + "s", // 18 + "sdg", // 19 + "t", // 20 + "tdg", // 21 + "sxdg", // 22 + "iswap", // 23 + "xx_minus_yy", // 24 + "xx_plus_yy", // 25 + "u1", // 26 + "u2", // 27 + "u3", // 28 + "crx", // 29 + "cry", // 30 + "crz", // 31 + "r", // 32 + "ch", // 33 + "cp", // 34 + "cs", // 35 + "csdg", // 36 + "csx", // 37 + "cswap", // 38 + "cu", // 39 + "cu1", // 40 + "cu3", // 41 + "c3x", // 42 + "c3sx", // 43 + "c4x", // 44 + "dcx", // 45 + "ccz", // 46 + "rccx", // 47 + "rc3x", // 48 + "rxx", // 49 + "ryy", // 50 + "rzz", // 51 + "rzx", // 52 ]; #[pymethods] @@ -296,7 +356,7 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 29; +pub const STANDARD_GATE_SIZE: usize = 53; impl Operation for StandardGate { fn name(&self) -> &str { @@ -453,6 +513,21 @@ impl Operation for StandardGate { } _ => None, }, + Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), + Self::RGate => todo!(), + Self::CHGate => todo!(), + Self::CPhaseGate => todo!(), + Self::CSGate => todo!(), + Self::CSdgGate => todo!(), + Self::CSXGate => todo!(), + Self::CSwapGate => todo!(), + Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::DCXGate => todo!(), + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), + Self::RZXGate => todo!(), } } @@ -878,6 +953,23 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), + Self::RGate => todo!(), + Self::CHGate => todo!(), + Self::CPhaseGate => todo!(), + Self::CSGate => todo!(), + Self::CSdgGate => todo!(), + Self::CSXGate => todo!(), + Self::CSwapGate => todo!(), + Self::CUGate => todo!(), + Self::CU1Gate => todo!(), + Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::DCXGate => todo!(), + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), + Self::RZXGate => todo!(), } } From 8b1f75ffafc70596bcf45480aa2f6d59d822e337 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 24 Jun 2024 19:21:20 +0100 Subject: [PATCH 138/159] Deprecate tuple-like access to `CircuitInstruction` (#12640) This has been the legacy path since `CircuitInstruction` was added in gh-8093. It's more performant to use the attribute-access patterns, and with more of the internals moving to Rust and potentially needing more use of additional class methods and attributes, we need to start shifting people away from the old form. --- crates/circuit/src/circuit_instruction.rs | 39 +++++++++++++++++-- crates/circuit/src/imports.rs | 2 + ...-circuit-instruction-8a332ab09de73766.yaml | 23 +++++++++++ test/python/circuit/test_circuit_data.py | 6 ++- test/utils/base.py | 9 +++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 93e73ccbc42..781a776c156 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use pyo3::basic::CompareOp; -use pyo3::exceptions::PyValueError; +use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; @@ -22,7 +22,7 @@ use smallvec::{smallvec, SmallVec}; use crate::imports::{ get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, - SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, }; use crate::interner::Index; use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; @@ -572,26 +572,31 @@ impl CircuitInstruction { #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } #[cfg(feature = "cache_pygates")] pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } #[cfg(not(feature = "cache_pygates"))] pub fn __iter__(&self, py: Python<'_>) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } #[cfg(feature = "cache_pygates")] pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } - pub fn __len__(&self) -> usize { - 3 + pub fn __len__(&self, py: Python) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(3) } pub fn __richcmp__( @@ -939,3 +944,29 @@ pub(crate) fn convert_py_to_operation_type( } Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) } + +/// Issue a Python `DeprecationWarning` about using the legacy tuple-like interface to +/// `CircuitInstruction`. +/// +/// Beware the `stacklevel` here doesn't work quite the same way as it does in Python as Rust-space +/// calls are completely transparent to Python. +#[inline] +fn warn_on_legacy_circuit_instruction_iteration(py: Python) -> PyResult<()> { + WARNINGS_WARN + .get_bound(py) + .call1(( + intern!( + py, + concat!( + "Treating CircuitInstruction as an iterable is deprecated legacy behavior", + " since Qiskit 1.2, and will be removed in Qiskit 2.0.", + " Instead, use the `operation`, `qubits` and `clbits` named attributes." + ) + ), + py.get_type_bound::(), + // Stack level. Compared to Python-space calls to `warn`, this is unusually low + // beacuse all our internal call structure is now Rust-space and invisible to Python. + 1, + )) + .map(|_| ()) +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 76e808d1b30..92700f3274e 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -72,6 +72,8 @@ pub static SINGLETON_GATE: ImportOnceCell = pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); + /// 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 /// when a gate is added directly via the StandardGate path and there isn't a Python object diff --git a/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml new file mode 100644 index 00000000000..d656ee5cb82 --- /dev/null +++ b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml @@ -0,0 +1,23 @@ +--- +deprecations_circuits: + - | + Treating :class:`.CircuitInstruction` as a tuple-like iterable is deprecated, and this legacy + path way will be removed in Qiskit 2.0. You should use the attribute-access fields + :attr:`~.CircuitInstruction.operation`, :attr:`~.CircuitInstruction.qubits`, and + :attr:`~.CircuitInstruction.clbits` instead. For example:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + + # Deprecated. + for op, qubits, clbits in qc.data: + pass + # New style. + for instruction in qc.data: + op = instruction.operation + qubits = instruction.qubits + clbits = instruction.clbits diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 35ae27b2fcf..55028c8e883 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -416,7 +416,11 @@ def to_legacy(instruction): return (instruction.operation, list(instruction.qubits), list(instruction.clbits)) expected = [to_legacy(instruction) for instruction in qc.data] - actual = [tuple(instruction) for instruction in qc.data] + + with self.assertWarnsRegex( + DeprecationWarning, "Treating CircuitInstruction as an iterable is deprecated" + ): + actual = [tuple(instruction) for instruction in qc.data] self.assertEqual(actual, expected) def test_getitem_by_insertion_order(self): diff --git a/test/utils/base.py b/test/utils/base.py index 63a8bf4384f..bebf0300885 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -215,6 +215,15 @@ def setUpClass(cls): module=r"seaborn(\..*)?", ) + # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version + # of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + message="Treating CircuitInstruction as an iterable is deprecated", + module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", + ) + allow_DeprecationWarning_modules = [ "test.python.pulse.test_builder", "test.python.pulse.test_block", From 1ed5951a98b594808525c8428e06178c160cfcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:53:54 +0200 Subject: [PATCH 139/159] Pin scipy to 1.13.1 to bypass CI failures (#12654) * Pin scipy to 1.13.1 to bypass CI failures * whoops * double whoops --- constraints.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/constraints.txt b/constraints.txt index d3985581d36..6681de226d9 100644 --- a/constraints.txt +++ b/constraints.txt @@ -3,6 +3,10 @@ # https://github.com/Qiskit/qiskit-terra/issues/10345 for current details. scipy<1.11; python_version<'3.12' +# Temporary pin to avoid CI issues caused by scipy 1.14.0 +# See https://github.com/Qiskit/qiskit/issues/12655 for current details. +scipy==1.13.1; python_version=='3.12' + # z3-solver from 4.12.3 onwards upped the minimum macOS API version for its # wheels to 11.7. The Azure VM images contain pre-built CPythons, of which at # least CPython 3.8 was compiled for an older macOS, so does not match a From 6974b4500f6c716b407b599e5cec80afbb757516 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 26 Jun 2024 08:29:34 +0200 Subject: [PATCH 140/159] Fix some bugs in loading Solovay Kitaev decompositions (#12579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix store & load - fix access via .item() - fix storing of global phase - fix storing ofgate sequence labels * undangle a dangling print * fix import order * Update releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml 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> --- .../generate_basis_approximations.py | 2 +- .../discrete_basis/solovay_kitaev.py | 24 ++++++++++---- ...ix-sk-load-from-file-02c6eabbbd7fcda3.yaml | 10 ++++++ test/python/transpiler/test_solovay_kitaev.py | 31 +++++++++++++++++++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml diff --git a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py index 672d0eb9e8e..da9708c2455 100644 --- a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py +++ b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py @@ -156,7 +156,7 @@ def generate_basic_approximations( data = {} for sequence in sequences: gatestring = sequence.name - data[gatestring] = sequence.product + data[gatestring] = (sequence.product, sequence.global_phase) np.save(filename, data) diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 2c8df5bd1b6..f367f6c0f0b 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -51,14 +51,19 @@ def __init__( self.basic_approximations = self.load_basic_approximations(basic_approximations) - def load_basic_approximations(self, data: list | str | dict) -> list[GateSequence]: + @staticmethod + def load_basic_approximations(data: list | str | dict) -> list[GateSequence]: """Load basic approximations. Args: data: If a string, specifies the path to the file from where to load the data. - If a dictionary, directly specifies the decompositions as ``{gates: matrix}``. - There ``gates`` are the names of the gates producing the SO(3) matrix ``matrix``, - e.g. ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}``. + If a dictionary, directly specifies the decompositions as ``{gates: matrix}`` + or ``{gates: (matrix, global_phase)}``. There, ``gates`` are the names of the gates + producing the SO(3) matrix ``matrix``, e.g. + ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}`` + and the ``global_phase`` can be given to account for a global phase difference + between the U(2) matrix of the quantum gates and the stored SO(3) matrix. + If not given, the ``global_phase`` will be assumed to be 0. Returns: A list of basic approximations as type ``GateSequence``. @@ -72,13 +77,20 @@ def load_basic_approximations(self, data: list | str | dict) -> list[GateSequenc # if a file, load the dictionary if isinstance(data, str): - data = np.load(data, allow_pickle=True) + data = np.load(data, allow_pickle=True).item() sequences = [] - for gatestring, matrix in data.items(): + for gatestring, matrix_and_phase in data.items(): + if isinstance(matrix_and_phase, tuple): + matrix, global_phase = matrix_and_phase + else: + matrix, global_phase = matrix_and_phase, 0 + sequence = GateSequence() sequence.gates = [_1q_gates[element] for element in gatestring.split()] + sequence.labels = [gate.name for gate in sequence.gates] sequence.product = np.asarray(matrix) + sequence.global_phase = global_phase sequences.append(sequence) return sequences diff --git a/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml new file mode 100644 index 00000000000..d995af06bcc --- /dev/null +++ b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix the :class:`.SolovayKitaev` transpiler pass when loading basic + approximations from an exising ``.npy`` file. Previously, loading + a stored approximation which allowed for further reductions (e.g. due + to gate cancellations) could cause a runtime failure. + Additionally, the global phase difference of the U(2) gate product + and SO(3) representation was lost during a save-reload procedure. + Fixes `Qiskit/qiskit#12576 `_. diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index e15a080f6f0..62b811c8e3b 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -12,8 +12,10 @@ """Test the Solovay Kitaev transpilation pass.""" +import os import unittest import math +import tempfile import numpy as np import scipy @@ -230,6 +232,35 @@ def test_u_gates_work(self): included_gates = set(discretized.count_ops().keys()) self.assertEqual(set(basis_gates), included_gates) + def test_load_from_file(self): + """Test loading basic approximations from a file works. + + Regression test of Qiskit/qiskit#12576. + """ + filename = "approximations.npy" + + with tempfile.TemporaryDirectory() as tmp_dir: + fullpath = os.path.join(tmp_dir, filename) + + # dump approximations to file + generate_basic_approximations(basis_gates=["h", "s", "sdg"], depth=3, filename=fullpath) + + # circuit to decompose and reference decomp + circuit = QuantumCircuit(1) + circuit.rx(0.8, 0) + + reference = QuantumCircuit(1, global_phase=3 * np.pi / 4) + reference.h(0) + reference.s(0) + reference.h(0) + + # load the decomp and compare to reference + skd = SolovayKitaev(basic_approximations=fullpath) + # skd = SolovayKitaev(basic_approximations=filename) + discretized = skd(circuit) + + self.assertEqual(discretized, reference) + @ddt class TestGateSequence(QiskitTestCase): From e36027c01a5d18b72225502c0fd5021613893623 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 26 Jun 2024 09:41:11 +0200 Subject: [PATCH 141/159] GenericBackendV2 should fail when the backend cannot allocate the basis gate because its size (#12653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GenericBackendV2 should fail when the backend cannot allocate the basis gate because its size Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * reno * Update releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * another single qubit backend --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/providers/fake_provider/generic_backend_v2.py | 5 +++++ .../notes/fixes_GenericBackendV2-668e40596e1f070d.yaml | 4 ++++ .../providers/fake_provider/test_generic_backend_v2.py | 10 ++++++++++ test/visual/mpl/graph/test_graph_matplotlib_drawer.py | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 1ac0484d775..214754080e5 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -375,6 +375,11 @@ def _build_generic_target(self): f"in the standard qiskit circuit library." ) gate = self._supported_gates[name] + if self.num_qubits < gate.num_qubits: + raise QiskitError( + f"Provided basis gate {name} needs more qubits than {self.num_qubits}, " + f"which is the size of the backend." + ) noise_params = self._get_noise_defaults(name, gate.num_qubits) self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) diff --git a/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml new file mode 100644 index 00000000000..9d297125e3c --- /dev/null +++ b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + The constructor :class:`.GenericBackendV2` was allowing to create malformed backends because it accepted basis gates that couldn't be allocated in the backend size . That is, a backend with a single qubit should not accept a basis with two-qubit gates. 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 b4fbe944c33..cd7c611b221 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -35,6 +35,16 @@ def test_supported_basis_gates(self): with self.assertRaises(QiskitError): GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"]) + 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"]) + + 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"]) + def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" diff --git a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py index ae69f212f89..20fae107d30 100644 --- a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py +++ b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py @@ -389,7 +389,7 @@ def test_plot_1_qubit_gate_map(self): """Test plot_gate_map using 1 qubit backend""" # getting the mock backend from FakeProvider - backend = GenericBackendV2(num_qubits=1) + backend = GenericBackendV2(num_qubits=1, basis_gates=["id", "rz", "sx", "x"]) fname = "1_qubit_gate_map.png" self.graph_plot_gate_map(backend=backend, filename=fname) From 4d3821b01a70c40e8542646ad92759aae877a096 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Jun 2024 08:06:23 -0400 Subject: [PATCH 142/159] Simplify QuantumCircuit._from_circuit_data bit handling (#12661) * Simplify QuantumCircuit._from_circuit_data bit handling This commit simplifies the logic around bit handling in the `QuantumCircuit._from_circuit_data()` constructor. Previously it was calling `add_bits()` for each bit in the `CircuitData` object to update the output circuit's accounting for each qubit. But this was needlessly heavy as the `CircuitData` is already the source of truth for the bits in a circuit and we just need to update the indices dictionary. The `add_bits()` method attempts to add the bits to the `CircuitData` too but this is wasted overhead because the `CircuitData` already has the bits as that's where the came from. This changes the constructor to just directly set the bit indices as needed and return the circuit. * Use a dict comprehension instead of a for loop --- qiskit/circuit/quantumcircuit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ee52e3308a9..8b3ff7bf197 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1161,9 +1161,9 @@ def __init__( def _from_circuit_data(cls, data: CircuitData) -> typing.Self: """A private constructor from rust space circuit data.""" out = QuantumCircuit() - out.add_bits(data.qubits) - out.add_bits(data.clbits) out._data = data + out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} + out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} return out @staticmethod From 26680dcecf9457210f46aa6b2f62361a5ccd8d84 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 26 Jun 2024 14:45:24 +0200 Subject: [PATCH 143/159] adapting test/randomized/test_transpiler_equivalence.py to #12640 (#12663) * addapting to #12640 * more instances --- test/randomized/test_transpiler_equivalence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index 04dced90dfa..3bd09d89348 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -258,9 +258,9 @@ def add_c_if_last_gate(self, carg, data): last_gate = self.qc.data[-1] # Conditional instructions are not supported - assume(isinstance(last_gate[0], Gate)) + assume(isinstance(last_gate.operation, Gate)) - last_gate[0].c_if(creg, val) + last_gate.operation.c_if(creg, val) # Properties to check @@ -269,7 +269,7 @@ def qasm(self): """After each circuit operation, it should be possible to build QASM.""" qasm2.dumps(self.qc) - @precondition(lambda self: any(isinstance(d[0], Measure) for d in self.qc.data)) + @precondition(lambda self: any(isinstance(d.operation, Measure) for d in self.qc.data)) @rule(kwargs=transpiler_conf()) def equivalent_transpile(self, kwargs): """Simulate, transpile and simulate the present circuit. Verify that the From 39b2c90b813da63d006755580cabd80b718b74bb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Jun 2024 09:48:25 -0400 Subject: [PATCH 144/159] Add test case to validate the rust->Python gate conversion (#12623) This commit adds a test to the test_rust_equivalence module to assert that the Python gate objects returned from the Rust CircuitData is the correct type. --- test/python/circuit/test_rust_equivalence.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index 8d6d159c0b6..b20db4c79f9 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -18,7 +18,7 @@ import numpy as np -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, CircuitInstruction from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping SKIP_LIST = {"rx", "ry", "ecr"} @@ -39,6 +39,21 @@ def setUp(self): gate = gate.base_class(*[pi] * len(gate.params)) qc.append(gate, list(range(gate.num_qubits))) + def test_gate_cross_domain_conversion(self): + """Test the rust -> python conversion returns the right class.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # Gate not in rust yet or no constructor method + continue + with self.subTest(name=name): + qc = QuantumCircuit(standard_gate.num_qubits) + qc._append( + CircuitInstruction(standard_gate, qubits=qc.qubits, params=gate_class.params) + ) + self.assertEqual(qc.data[0].operation.base_class, gate_class.base_class) + self.assertEqual(qc.data[0].operation, gate_class) + def test_definitions(self): """Test definitions are the same in rust space.""" for name, gate_class in self.standard_gates.items(): From 2fab2007e3d7384e9e55a10340952c4974ece03c Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:15:10 +0300 Subject: [PATCH 145/159] Add Rust representation for DCXGate (#12644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updating tables * Adding remaining code * Appending the Rust representation directly * Fix fmt --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/gate_matrix.rs | 7 ++++++ crates/circuit/src/imports.rs | 2 +- crates/circuit/src/operations.rs | 25 ++++++++++++++++---- qiskit/circuit/library/standard_gates/dcx.py | 3 +++ qiskit/circuit/quantumcircuit.py | 4 +--- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 2e5f55d6ddc..80fecfb597c 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -226,6 +226,13 @@ pub static TDG_GATE: [[Complex64; 2]; 2] = [ [c64(0., 0.), c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], ]; +pub static DCX_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], +]; + #[inline] pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { [[c64(0., theta).exp()]] diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 92700f3274e..632f5b0f573 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -183,7 +183,7 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // C4XGate = 44 ["placeholder", "placeholder"], // DCXGate = 45 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.dcx", "DCXGate"], // CCZGate = 46 ["placeholder", "placeholder"], // RCCXGate = 47 diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index af7dabc8621..d9626b5c737 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -240,7 +240,7 @@ static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 34, 34, 34, 2, 2, 2, 2, 2, 3, 2, // 30-39 - 2, 2, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 2, 2, 34, 34, 34, 2, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -250,7 +250,7 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 34, 34, 34, 0, 1, 0, 0, 0, 0, 3, // 30-39 - 1, 3, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 1, 3, 34, 34, 34, 0, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -523,7 +523,10 @@ impl Operation for StandardGate { Self::CSwapGate => todo!(), Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), - Self::DCXGate => todo!(), + Self::DCXGate => match params { + [] => Some(aview2(&gate_matrix::DCX_GATE).to_owned()), + _ => None, + }, Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), @@ -965,7 +968,21 @@ impl Operation for StandardGate { Self::CU1Gate => todo!(), Self::CU3Gate => todo!(), Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), - Self::DCXGate => todo!(), + Self::DCXGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index 6455bea2779..d83f2e2f9c7 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -15,6 +15,7 @@ from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]]) @@ -48,6 +49,8 @@ class DCXGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.DCXGate + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new DCX gate.""" super().__init__("dcx", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 8b3ff7bf197..08bac04c9e6 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -5328,9 +5328,7 @@ def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.dcx import DCXGate - - return self.append(DCXGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(op=StandardGate.DCXGate, qargs=[qubit1, qubit2]) def ccx( self, From 6447941885254066371b674334e68ee153e5b329 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Thu, 27 Jun 2024 05:08:24 -0400 Subject: [PATCH 146/159] Implement RGate in Rust (#12662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement RGate in Rust * Update crates/circuit/src/operations.rs Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Fix error in decomposition of RGate There is an error in the expression for decomposition of the R gate in the port to Rust. This fixes the error and re-enables the skipped test that failed because of the incorrect expression. * Factor cloning the Param enum in Rust To clone the enum, each variant must be handled separately. This is currently used once, but can be used each time a `Param` is cloned. In case more work needs to be done within match arms, one might choose not to use this function, but rather clone in each of these arms. * Run cargo fmt * Implement and use addition for enum Param This handles `Float` and `ParameterExpression` variants uniformly. --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/gate_matrix.rs | 13 ++++++ crates/circuit/src/imports.rs | 2 +- crates/circuit/src/operations.rs | 52 +++++++++++++++++++--- qiskit/circuit/library/standard_gates/r.py | 3 ++ qiskit/circuit/quantumcircuit.py | 4 +- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 80fecfb597c..2a3fcdf8828 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -24,6 +24,19 @@ const fn c64(re: f64, im: f64) -> Complex64 { pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(1., 0.)]]; +#[inline] +pub fn r_gate(theta: f64, phi: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cost = c64(half_theta.cos(), 0.); + let sint = half_theta.sin(); + let cosphi = phi.cos(); + let sinphi = phi.sin(); + [ + [cost, c64(-sint * sinphi, -sint * cosphi)], + [c64(sint * sinphi, -sint * cosphi), cost], + ] +} + #[inline] pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { let half_theta = theta / 2.; diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 632f5b0f573..bf06685ba53 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -157,7 +157,7 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // CRZGate = 31 ["placeholder", "placeholder"], // RGate 32 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.r", "RGate"], // CHGate = 33 ["qiskit.circuit.library.standard_gates.h", "CHGate"], // CPhaseGate = 34 diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index d9626b5c737..e0e93726735 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -239,7 +239,7 @@ static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 - 34, 34, 34, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 34, 34, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 2, 2, 34, 34, 34, 2, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -249,7 +249,7 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 - 34, 34, 34, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 34, 34, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 1, 3, 34, 34, 34, 0, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -514,7 +514,12 @@ impl Operation for StandardGate { _ => None, }, Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), - Self::RGate => todo!(), + Self::RGate => match params { + [Param::Float(theta), Param::Float(phi)] => { + Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) + } + _ => None, + }, Self::CHGate => todo!(), Self::CPhaseGate => todo!(), Self::CSGate => todo!(), @@ -957,7 +962,21 @@ impl Operation for StandardGate { ) }), Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), - Self::RGate => todo!(), + Self::RGate => Python::with_gil(|py| -> Option { + let theta_expr = clone_param(¶ms[0], py); + let phi_expr1 = add_param(¶ms[1], -PI2, py); + let phi_expr2 = multiply_param(&phi_expr1, -1.0, py); + let defparams = smallvec![theta_expr, phi_expr1, phi_expr2]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [(Self::UGate, defparams, smallvec![Qubit(0)])], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::CHGate => todo!(), Self::CPhaseGate => todo!(), Self::CSGate => todo!(), @@ -997,6 +1016,16 @@ impl Operation for StandardGate { const FLOAT_ZERO: Param = Param::Float(0.0); +// Return explictly requested copy of `param`, handling +// each variant separately. +fn clone_param(param: &Param, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta), + Param::ParameterExpression(theta) => Param::ParameterExpression(theta.clone_ref(py)), + Param::Obj(_) => unreachable!(), + } +} + fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { match param { Param::Float(theta) => Param::Float(*theta * mult), @@ -1004,7 +1033,20 @@ fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { theta .clone_ref(py) .call_method1(py, intern!(py, "__rmul__"), (mult,)) - .expect("Parameter expression for global phase failed"), + .expect("Multiplication of Parameter expression by float failed."), + ), + Param::Obj(_) => unreachable!(), + } +} + +fn add_param(param: &Param, summand: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta + summand), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (summand,)) + .expect("Sum of Parameter expression and float failed."), ), Param::Obj(_) => unreachable!(), } diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 9d4905e2786..22c30e24bf6 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RGate(Gate): @@ -49,6 +50,8 @@ class RGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 08bac04c9e6..de7a2934a46 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4649,9 +4649,7 @@ def r( Returns: A handle to the instructions created. """ - from .library.standard_gates.r import RGate - - return self.append(RGate(theta, phi), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RGate, [theta, phi], qargs=[qubit]) def rv( self, From 76af5b475b9b2a57f9eeabd3c0b793b037464630 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:58:52 +0200 Subject: [PATCH 147/159] Enable the new efficient MCX decompose (#12628) * enable the new efficient MCX decompose * fix tests * revert explicit * apply review comments * update test_circuit_qasm.py * update test_decompose.py * revert C3X C4X names * fix qasm2 exporter tests use regex to fetch the mcx_ name * fix lint and add reno --------- Co-authored-by: Julien Gacon --- qiskit/circuit/quantumcircuit.py | 4 ++-- .../notes/fix-mcx-performance-de86bcc9f969b81e.yaml | 6 ++++++ test/python/circuit/test_circuit_qasm.py | 12 +++++++----- test/python/circuit/test_controlled_gate.py | 4 ++-- test/python/qasm2/test_export.py | 13 +++++++------ test/python/transpiler/test_decompose.py | 4 ++-- 6 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index de7a2934a46..485591a8a3b 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -5402,12 +5402,12 @@ def mcx( ValueError: if the given mode is not known, or if too few ancilla qubits are passed. AttributeError: if no ancilla qubits are passed, but some are needed. """ - from .library.standard_gates.x import MCXGrayCode, MCXRecursive, MCXVChain + from .library.standard_gates.x import MCXGate, MCXRecursive, MCXVChain num_ctrl_qubits = len(control_qubits) available_implementations = { - "noancilla": MCXGrayCode(num_ctrl_qubits, ctrl_state=ctrl_state), + "noancilla": MCXGate(num_ctrl_qubits, ctrl_state=ctrl_state), "recursion": MCXRecursive(num_ctrl_qubits, ctrl_state=ctrl_state), "v-chain": MCXVChain(num_ctrl_qubits, False, ctrl_state=ctrl_state), "v-chain-dirty": MCXVChain(num_ctrl_qubits, dirty_ancillas=True, ctrl_state=ctrl_state), diff --git a/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml b/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml new file mode 100644 index 00000000000..8cee3356ac4 --- /dev/null +++ b/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Improve the decomposition of the gate generated by :meth:`.QuantumCircuit.mcx` + without using ancilla qubits, so that the number of :class:`.CXGate` will grow + quadratically in the number of qubits and not exponentially. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index c1ece0230d3..13882281cff 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -394,12 +394,14 @@ def test_circuit_qasm_with_mcx_gate(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - expected_qasm = """OPENQASM 2.0; + pattern = r"""OPENQASM 2.0; include "qelib1.inc"; -gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; } -qreg q[4]; -mcx q[0],q[1],q[2],q[3];""" - self.assertEqual(dumps(qc), expected_qasm) +gate mcx q0,q1,q2,q3 { h q3; p\(pi/8\) q0; p\(pi/8\) q1; p\(pi/8\) q2; p\(pi/8\) q3; cx q0,q1; p\(-pi/8\) q1; cx q0,q1; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; p\(pi/8\) q2; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; h q3; } +gate (?Pmcx_[0-9]*) q0,q1,q2,q3 { mcx q0,q1,q2,q3; } +qreg q\[4\]; +(?P=mcx_id) q\[0\],q\[1\],q\[2\],q\[3\];""" + expected_qasm = re.compile(pattern, re.MULTILINE) + self.assertRegex(dumps(qc), expected_qasm) def test_circuit_qasm_with_mcx_gate_variants(self): """Test circuit qasm() method with MCXGrayCode, MCXRecursive, MCXVChain""" diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 8ba70ee852c..f26ab987f4f 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -764,9 +764,9 @@ def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits): @data(1, 2, 3, 4) def test_mcxgraycode_gates_yield_explicit_gates(self, num_ctrl_qubits): - """Test creating an mcx gate calls MCXGrayCode and yeilds explicit definition.""" + """Test an MCXGrayCode yields explicit definition.""" qc = QuantumCircuit(num_ctrl_qubits + 1) - qc.mcx(list(range(num_ctrl_qubits)), [num_ctrl_qubits]) + qc.append(MCXGrayCode(num_ctrl_qubits), list(range(qc.num_qubits)), []) explicit = {1: CXGate, 2: CCXGate, 3: C3XGate, 4: C4XGate} self.assertEqual(type(qc[0].operation), explicit[num_ctrl_qubits]) diff --git a/test/python/qasm2/test_export.py b/test/python/qasm2/test_export.py index a0a3ade6ce8..85172ec3ce8 100644 --- a/test/python/qasm2/test_export.py +++ b/test/python/qasm2/test_export.py @@ -387,13 +387,14 @@ def test_mcx_gate(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - expected_qasm = """\ -OPENQASM 2.0; + pattern = r"""OPENQASM 2.0; include "qelib1.inc"; -gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; } -qreg q[4]; -mcx q[0],q[1],q[2],q[3];""" - self.assertEqual(qasm2.dumps(qc), expected_qasm) +gate mcx q0,q1,q2,q3 { h q3; p\(pi/8\) q0; p\(pi/8\) q1; p\(pi/8\) q2; p\(pi/8\) q3; cx q0,q1; p\(-pi/8\) q1; cx q0,q1; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; p\(pi/8\) q2; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; h q3; } +gate (?Pmcx_[0-9]*) q0,q1,q2,q3 { mcx q0,q1,q2,q3; } +qreg q\[4\]; +(?P=mcx_id) q\[0\],q\[1\],q\[2\],q\[3\];""" + expected_qasm = re.compile(pattern, re.MULTILINE) + self.assertRegex(qasm2.dumps(qc), expected_qasm) def test_mcx_gate_variants(self): n = 5 diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 91ebede9fa8..7b364f3ac10 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -216,7 +216,7 @@ def test_decompose_only_given_label(self): def test_decompose_only_given_name(self): """Test decomposition parameters so that only given name is decomposed.""" - decom_circ = self.complex_circuit.decompose(["mcx"]) + decom_circ = self.complex_circuit.decompose(["mcx"], reps=2) dag = circuit_to_dag(decom_circ) self.assertEqual(len(dag.op_nodes()), 13) @@ -236,7 +236,7 @@ def test_decompose_only_given_name(self): def test_decompose_mixture_of_names_and_labels(self): """Test decomposition parameters so that mixture of names and labels is decomposed""" - decom_circ = self.complex_circuit.decompose(["mcx", "gate2"]) + decom_circ = self.complex_circuit.decompose(["mcx", "gate2"], reps=2) dag = circuit_to_dag(decom_circ) self.assertEqual(len(dag.op_nodes()), 15) From ea5a54b9da8db91a3e67812856ce419bef923822 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Thu, 27 Jun 2024 15:38:36 -0400 Subject: [PATCH 148/159] Add constant abbreviations for some values and types. (#12651) This PR introduces some abbreviations for repetitive Rust code. Motivations are reducing clutter, improving readability, and perhaps modest support for rapid development. * Use the definition of `const fn 64` that was introduced in #12459 uniformly in all crates. * Define some complex constants `C_ONE`, `C_ZERO`, `IM`, etc. * Introduce type definitions for arrays representing gates. For example: `GateArray1Q = [[Complex64; 2]; 2];` --- .../accelerate/src/convert_2q_block_matrix.rs | 5 +- .../src/euler_one_qubit_decomposer.rs | 11 +- crates/accelerate/src/isometry.rs | 7 +- crates/accelerate/src/pauli_exp_val.rs | 5 +- crates/accelerate/src/sampled_exp_val.rs | 3 +- crates/accelerate/src/sparse_pauli_op.rs | 29 +- crates/accelerate/src/two_qubit_decompose.rs | 211 +++++---------- crates/accelerate/src/uc_gate.rs | 14 +- crates/circuit/src/gate_matrix.rs | 254 +++++++----------- crates/circuit/src/lib.rs | 1 + crates/circuit/src/operations.rs | 37 +-- crates/circuit/src/util.rs | 48 ++++ 12 files changed, 262 insertions(+), 363 deletions(-) create mode 100644 crates/circuit/src/util.rs diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index e311c129b11..9c179397d64 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -20,10 +20,7 @@ use numpy::ndarray::{aview2, Array2, ArrayView2}; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; use smallvec::SmallVec; -static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; /// Return the matrix Operator resulting from a block of Instructions. #[pyfunction] diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 1fd5fd7834f..9f10f76de46 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -31,6 +31,7 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use qiskit_circuit::util::c64; use qiskit_circuit::SliceOrInt; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -855,16 +856,16 @@ pub fn params_xyx(unitary: PyReadonlyArray2) -> [f64; 4] { fn params_xzx_inner(umat: ArrayView2) -> [f64; 4] { let det = det_one_qubit(umat); - let phase = (Complex64::new(0., -1.) * det.ln()).re / 2.; + let phase = det.ln().im / 2.; let sqrt_det = det.sqrt(); let mat_zyz = arr2(&[ [ - Complex64::new((umat[[0, 0]] / sqrt_det).re, (umat[[1, 0]] / sqrt_det).im), - Complex64::new((umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), + c64((umat[[0, 0]] / sqrt_det).re, (umat[[1, 0]] / sqrt_det).im), + c64((umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), ], [ - Complex64::new(-(umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), - Complex64::new((umat[[0, 0]] / sqrt_det).re, -(umat[[1, 0]] / sqrt_det).im), + c64(-(umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), + c64((umat[[0, 0]] / sqrt_det).re, -(umat[[1, 0]] / sqrt_det).im), ], ]); let [theta, phi, lam, phase_zxz] = params_zxz_inner(mat_zyz.view()); diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index a3a8be38dae..ceaba2946b3 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -24,6 +24,7 @@ use ndarray::prelude::*; use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; +use qiskit_circuit::util::C_ZERO; /// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or /// basis_state=1 respectively @@ -315,11 +316,7 @@ pub fn merge_ucgate_and_diag( .enumerate() .map(|(i, raw_gate)| { let gate = raw_gate.as_array(); - let res = aview2(&[ - [diag[2 * i], Complex64::new(0., 0.)], - [Complex64::new(0., 0.), diag[2 * i + 1]], - ]) - .dot(&gate); + let res = aview2(&[[diag[2 * i], C_ZERO], [C_ZERO, diag[2 * i + 1]]]).dot(&gate); res.into_pyarray_bound(py).into() }) .collect() diff --git a/crates/accelerate/src/pauli_exp_val.rs b/crates/accelerate/src/pauli_exp_val.rs index 52a2fc07f81..8ee4b019b3e 100644 --- a/crates/accelerate/src/pauli_exp_val.rs +++ b/crates/accelerate/src/pauli_exp_val.rs @@ -19,6 +19,7 @@ use pyo3::wrap_pyfunction; use rayon::prelude::*; use crate::getenv_use_multiple_threads; +use qiskit_circuit::util::c64; const PARALLEL_THRESHOLD: usize = 19; @@ -88,7 +89,7 @@ pub fn expval_pauli_with_x( let index_0 = ((i << 1) & mask_u) | (i & mask_l); let index_1 = index_0 ^ x_mask; let val_0 = (phase - * Complex64::new( + * c64( data_arr[index_1].re * data_arr[index_0].re + data_arr[index_1].im * data_arr[index_0].im, data_arr[index_1].im * data_arr[index_0].re @@ -96,7 +97,7 @@ pub fn expval_pauli_with_x( )) .re; let val_1 = (phase - * Complex64::new( + * c64( data_arr[index_0].re * data_arr[index_1].re + data_arr[index_0].im * data_arr[index_1].im, data_arr[index_0].im * data_arr[index_1].re diff --git a/crates/accelerate/src/sampled_exp_val.rs b/crates/accelerate/src/sampled_exp_val.rs index b51ca3c98f0..0b8836a9416 100644 --- a/crates/accelerate/src/sampled_exp_val.rs +++ b/crates/accelerate/src/sampled_exp_val.rs @@ -18,6 +18,7 @@ use pyo3::prelude::*; use pyo3::wrap_pyfunction; use crate::pauli_exp_val::fast_sum; +use qiskit_circuit::util::c64; const OPER_TABLE_SIZE: usize = (b'Z' as usize) + 1; const fn generate_oper_table() -> [[f64; 2]; OPER_TABLE_SIZE] { @@ -81,7 +82,7 @@ pub fn sampled_expval_complex( let out: Complex64 = oper_strs .into_iter() .enumerate() - .map(|(idx, string)| coeff_arr[idx] * Complex64::new(bitstring_expval(&dist, string), 0.)) + .map(|(idx, string)| coeff_arr[idx] * c64(bitstring_expval(&dist, string), 0.)) .sum(); Ok(out.re) } diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index e0c80f71616..8a51d8ee781 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -23,6 +23,7 @@ use hashbrown::HashMap; use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; use num_complex::Complex64; use num_traits::Zero; +use qiskit_circuit::util::{c64, C_ONE, C_ZERO}; use rayon::prelude::*; use crate::rayon_ext::*; @@ -257,9 +258,9 @@ impl<'py> ZXPaulisView<'py> { let ys = (xs & zs).count_ones(); match (phase as u32 + ys) % 4 { 0 => coeff, - 1 => Complex64::new(coeff.im, -coeff.re), - 2 => Complex64::new(-coeff.re, -coeff.im), - 3 => Complex64::new(-coeff.im, coeff.re), + 1 => c64(coeff.im, -coeff.re), + 2 => c64(-coeff.re, -coeff.im), + 3 => c64(-coeff.im, coeff.re), _ => unreachable!(), } }) @@ -311,10 +312,10 @@ impl MatrixCompressedPaulis { .zip(self.z_like.drain(..)) .zip(self.coeffs.drain(..)) { - *hash_table.entry(key).or_insert(Complex64::new(0.0, 0.0)) += coeff; + *hash_table.entry(key).or_insert(C_ZERO) += coeff; } for ((x, z), coeff) in hash_table { - if coeff == Complex64::new(0.0, 0.0) { + if coeff.is_zero() { continue; } self.x_like.push(x); @@ -347,7 +348,7 @@ pub fn decompose_dense( let mut coeffs = vec![]; if num_qubits > 0 { decompose_dense_inner( - Complex64::new(1.0, 0.0), + C_ONE, num_qubits, &[], operator.as_array(), @@ -532,7 +533,7 @@ fn to_matrix_dense_inner(paulis: &MatrixCompressedPaulis, parallel: bool) -> Vec // Doing the initialization here means that when we're in parallel contexts, we do the // zeroing across the whole threadpool. This also seems to give a speed-up in serial // contexts, but I don't understand that. ---Jake - row.fill(Complex64::new(0.0, 0.0)); + row.fill(C_ZERO); for ((&x_like, &z_like), &coeff) in paulis .x_like .iter() @@ -667,7 +668,7 @@ macro_rules! impl_to_matrix_sparse { ((i_row as $uint_ty) ^ (paulis.x_like[a] as $uint_ty)) .cmp(&((i_row as $uint_ty) ^ (paulis.x_like[b] as $uint_ty))) }); - let mut running = Complex64::new(0.0, 0.0); + let mut running = C_ZERO; let mut prev_index = i_row ^ (paulis.x_like[order[0]] as usize); for (x_like, z_like, coeff) in order .iter() @@ -748,7 +749,7 @@ macro_rules! impl_to_matrix_sparse { (i_row as $uint_ty ^ paulis.x_like[a] as $uint_ty) .cmp(&(i_row as $uint_ty ^ paulis.x_like[b] as $uint_ty)) }); - let mut running = Complex64::new(0.0, 0.0); + let mut running = C_ZERO; let mut prev_index = i_row ^ (paulis.x_like[order[0]] as usize); for (x_like, z_like, coeff) in order .iter() @@ -844,11 +845,11 @@ mod tests { // Deliberately using multiples of small powers of two so the floating-point addition // of them is associative. coeffs: vec![ - Complex64::new(0.25, 0.5), - Complex64::new(0.125, 0.25), - Complex64::new(0.375, 0.125), - Complex64::new(-0.375, 0.0625), - Complex64::new(-0.5, -0.25), + c64(0.25, 0.5), + c64(0.125, 0.25), + c64(0.375, 0.125), + c64(-0.375, 0.0625), + c64(-0.5, -0.25), ], } } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index e8c572b0403..8637cb03c73 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -52,67 +52,28 @@ use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; use qiskit_circuit::SliceOrInt; -const PI2: f64 = PI / 2.0; -const PI4: f64 = PI / 4.0; +const PI2: f64 = PI / 2.; +const PI4: f64 = PI / 4.; const PI32: f64 = 3.0 * PI2; const TWO_PI: f64 = 2.0 * PI; const C1: c64 = c64 { re: 1.0, im: 0.0 }; -static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [ - [ - Complex64::new(1.0, 0.), - Complex64::new(0., 1.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 1.), - Complex64::new(1.0, 0.0), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 1.), - Complex64::new(-1., 0.), - ], - [ - Complex64::new(1., 0.), - Complex64::new(0., -1.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], +static B_NON_NORMALIZED: GateArray2Q = [ + [C_ONE, IM, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, IM, C_ONE], + [C_ZERO, C_ZERO, IM, C_M_ONE], + [C_ONE, M_IM, C_ZERO, C_ZERO], ]; -static B_NON_NORMALIZED_DAGGER: [[Complex64; 4]; 4] = [ - [ - Complex64::new(0.5, 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0.5, 0.0), - ], - [ - Complex64::new(0., -0.5), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.5), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., -0.5), - Complex64::new(0., -0.5), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0.5, 0.), - Complex64::new(-0.5, 0.), - Complex64::new(0., 0.), - ], +static B_NON_NORMALIZED_DAGGER: GateArray2Q = [ + [c64(0.5, 0.), C_ZERO, C_ZERO, c64(0.5, 0.)], + [c64(0., -0.5), C_ZERO, C_ZERO, c64(0., 0.5)], + [C_ZERO, c64(0., -0.5), c64(0., -0.5), C_ZERO], + [C_ZERO, c64(0.5, 0.), c64(-0.5, 0.), C_ZERO], ]; enum MagicBasisTransform { @@ -318,29 +279,26 @@ fn closest_partial_swap(a: f64, b: f64, c: f64) -> f64 { fn rx_matrix(theta: f64) -> Array2 { let half_theta = theta / 2.; - let cos = Complex64::new(half_theta.cos(), 0.); - let isin = Complex64::new(0., -half_theta.sin()); + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., -half_theta.sin()); array![[cos, isin], [isin, cos]] } fn ry_matrix(theta: f64) -> Array2 { let half_theta = theta / 2.; - let cos = Complex64::new(half_theta.cos(), 0.); - let sin = Complex64::new(half_theta.sin(), 0.); + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); array![[cos, -sin], [sin, cos]] } fn rz_matrix(theta: f64) -> Array2 { - let ilam2 = Complex64::new(0., 0.5 * theta); - array![ - [(-ilam2).exp(), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), ilam2.exp()] - ] + let ilam2 = c64(0., 0.5 * theta); + array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); - let phase = Complex64::new(0., global_phase).exp(); + let phase = c64(0., global_phase).exp(); let mut matrix = Array2::from_diag(&arr1(&[phase, phase, phase, phase])); sequence .iter() @@ -375,7 +333,6 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2< } const DEFAULT_FIDELITY: f64 = 1.0 - 1.0e-9; -const C1_IM: Complex64 = Complex64::new(0.0, 1.0); #[derive(Clone, Debug, Copy)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose")] @@ -500,18 +457,9 @@ impl TwoQubitWeylDecomposition { } } -static IPZ: [[Complex64; 2]; 2] = [ - [C1_IM, Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(0., -1.)], -]; -static IPY: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(-1., 0.), Complex64::new(0., 0.)], -]; -static IPX: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), C1_IM], - [C1_IM, Complex64::new(0., 0.)], -]; +static IPZ: GateArray1Q = [[IM, C_ZERO], [C_ZERO, M_IM]]; +static IPY: GateArray1Q = [[C_ZERO, C_ONE], [C_M_ONE, C_ZERO]]; +static IPX: GateArray1Q = [[C_ZERO, IM], [IM, C_ZERO]]; #[pymethods] impl TwoQubitWeylDecomposition { @@ -671,7 +619,7 @@ impl TwoQubitWeylDecomposition { temp.diag_mut() .iter_mut() .enumerate() - .for_each(|(index, x)| *x = (C1_IM * d[index]).exp()); + .for_each(|(index, x)| *x = (IM * d[index]).exp()); let k1 = magic_basis_transform(u_p.dot(&p).dot(&temp).view(), MagicBasisTransform::Into); let k2 = magic_basis_transform(p.t(), MagicBasisTransform::Into); @@ -737,7 +685,7 @@ impl TwoQubitWeylDecomposition { let is_close = |ap: f64, bp: f64, cp: f64| -> bool { let [da, db, dc] = [a - ap, b - bp, c - cp]; let tr = 4. - * Complex64::new( + * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ); @@ -1016,13 +964,13 @@ impl TwoQubitWeylDecomposition { b - specialized.b, -c - specialized.c, ]; - 4. * Complex64::new( + 4. * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ) } else { let [da, db, dc] = [a - specialized.a, b - specialized.b, c - specialized.c]; - 4. * Complex64::new( + 4. * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ) @@ -1597,20 +1545,14 @@ impl TwoQubitBasisDecomposer { } } -static K12R_ARR: [[Complex64; 2]; 2] = [ - [ - Complex64::new(0., FRAC_1_SQRT_2), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(-FRAC_1_SQRT_2, 0.), - Complex64::new(0., -FRAC_1_SQRT_2), - ], +static K12R_ARR: GateArray1Q = [ + [c64(0., FRAC_1_SQRT_2), c64(FRAC_1_SQRT_2, 0.)], + [c64(-FRAC_1_SQRT_2, 0.), c64(0., -FRAC_1_SQRT_2)], ]; -static K12L_ARR: [[Complex64; 2]; 2] = [ - [Complex64::new(0.5, 0.5), Complex64::new(0.5, 0.5)], - [Complex64::new(-0.5, 0.5), Complex64::new(0.5, -0.5)], +static K12L_ARR: GateArray1Q = [ + [c64(0.5, 0.5), c64(0.5, 0.5)], + [c64(-0.5, 0.5), c64(0.5, -0.5)], ]; fn decomp0_inner(target: &TwoQubitWeylDecomposition) -> SmallVec<[Array2; 8]> { @@ -1650,90 +1592,71 @@ impl TwoQubitBasisDecomposer { // Create some useful matrices U1, U2, U3 are equivalent to the basis, // expand as Ui = Ki1.Ubasis.Ki2 let b = basis_decomposer.b; - let temp = Complex64::new(0.5, -0.5); + let temp = c64(0.5, -0.5); let k11l = array![ - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., -b).exp()), - temp * Complex64::new(0., -b).exp() - ], - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()), - temp * -(Complex64::new(0., b).exp()) - ], + [temp * (M_IM * c64(0., -b).exp()), temp * c64(0., -b).exp()], + [temp * (M_IM * c64(0., b).exp()), temp * -(c64(0., b).exp())], ]; let k11r = array![ [ - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * Complex64::new(0., -b).exp()), - FRAC_1_SQRT_2 * -Complex64::new(0., -b).exp() + FRAC_1_SQRT_2 * (IM * c64(0., -b).exp()), + FRAC_1_SQRT_2 * -c64(0., -b).exp() ], [ - FRAC_1_SQRT_2 * Complex64::new(0., b).exp(), - FRAC_1_SQRT_2 * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()) + FRAC_1_SQRT_2 * c64(0., b).exp(), + FRAC_1_SQRT_2 * (M_IM * c64(0., b).exp()) ], ]; let k12l = aview2(&K12L_ARR); let k12r = aview2(&K12R_ARR); let k32l_k21l = array![ [ - FRAC_1_SQRT_2 * Complex64::new(1., (2. * b).cos()), - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * (2. * b).sin()) + FRAC_1_SQRT_2 * c64(1., (2. * b).cos()), + FRAC_1_SQRT_2 * (IM * (2. * b).sin()) ], [ - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * (2. * b).sin()), - FRAC_1_SQRT_2 * Complex64::new(1., -(2. * b).cos()) + FRAC_1_SQRT_2 * (IM * (2. * b).sin()), + FRAC_1_SQRT_2 * c64(1., -(2. * b).cos()) ], ]; - let temp = Complex64::new(0.5, 0.5); + let temp = c64(0.5, 0.5); let k21r = array![ [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., -2. * b).exp()), - temp * Complex64::new(0., -2. * b).exp() + temp * (M_IM * c64(0., -2. * b).exp()), + temp * c64(0., -2. * b).exp() ], [ - temp * (Complex64::new(0., 1.) * Complex64::new(0., 2. * b).exp()), - temp * Complex64::new(0., 2. * b).exp() + temp * (IM * c64(0., 2. * b).exp()), + temp * c64(0., 2. * b).exp() ], ]; - const K22L_ARR: [[Complex64; 2]; 2] = [ - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(-FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], + const K22L_ARR: GateArray1Q = [ + [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], + [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], ]; let k22l = aview2(&K22L_ARR); - let k22r_arr: [[Complex64; 2]; 2] = [ - [Complex64::zero(), Complex64::new(1., 0.)], - [Complex64::new(-1., 0.), Complex64::zero()], - ]; + let k22r_arr: GateArray1Q = [[Complex64::zero(), C_ONE], [C_M_ONE, Complex64::zero()]]; let k22r = aview2(&k22r_arr); let k31l = array![ [ - FRAC_1_SQRT_2 * Complex64::new(0., -b).exp(), - FRAC_1_SQRT_2 * Complex64::new(0., -b).exp() + FRAC_1_SQRT_2 * c64(0., -b).exp(), + FRAC_1_SQRT_2 * c64(0., -b).exp() ], [ - FRAC_1_SQRT_2 * -Complex64::new(0., b).exp(), - FRAC_1_SQRT_2 * Complex64::new(0., b).exp() + FRAC_1_SQRT_2 * -c64(0., b).exp(), + FRAC_1_SQRT_2 * c64(0., b).exp() ], ]; - let temp = Complex64::new(0., 1.); let k31r = array![ - [temp * Complex64::new(0., b).exp(), Complex64::zero()], - [Complex64::zero(), temp * -Complex64::new(0., -b).exp()], + [IM * c64(0., b).exp(), Complex64::zero()], + [Complex64::zero(), M_IM * c64(0., -b).exp()], ]; - let temp = Complex64::new(0.5, 0.5); + let temp = c64(0.5, 0.5); let k32r = array![ + [temp * c64(0., b).exp(), temp * -c64(0., -b).exp()], [ - temp * Complex64::new(0., b).exp(), - temp * -Complex64::new(0., -b).exp() - ], - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()), - temp * (Complex64::new(0., -1.) * Complex64::new(0., -b).exp()) + temp * (M_IM * c64(0., b).exp()), + temp * (M_IM * c64(0., -b).exp()) ], ]; let k1ld = transpose_conjugate(basis_decomposer.K1l.view()); @@ -1793,11 +1716,11 @@ impl TwoQubitBasisDecomposer { fn traces(&self, target: &TwoQubitWeylDecomposition) -> [Complex64; 4] { [ - 4. * Complex64::new( + 4. * c64( target.a.cos() * target.b.cos() * target.c.cos(), target.a.sin() * target.b.sin() * target.c.sin(), ), - 4. * Complex64::new( + 4. * c64( (PI4 - target.a).cos() * (self.basis_decomposer.b - target.b).cos() * target.c.cos(), @@ -1805,8 +1728,8 @@ impl TwoQubitBasisDecomposer { * (self.basis_decomposer.b - target.b).sin() * target.c.sin(), ), - Complex64::new(4. * target.c.cos(), 0.), - Complex64::new(4., 0.), + c64(4. * target.c.cos(), 0.), + c64(4., 0.), ] } diff --git a/crates/accelerate/src/uc_gate.rs b/crates/accelerate/src/uc_gate.rs index 3a5f74a6f0b..21fd7fa0465 100644 --- a/crates/accelerate/src/uc_gate.rs +++ b/crates/accelerate/src/uc_gate.rs @@ -21,14 +21,14 @@ use ndarray::prelude::*; use numpy::{IntoPyArray, PyReadonlyArray2}; use crate::euler_one_qubit_decomposer::det_one_qubit; +use qiskit_circuit::util::{c64, C_ZERO, IM}; -const PI2: f64 = PI / 2.; const EPS: f64 = 1e-10; // These constants are the non-zero elements of an RZ gate's unitary with an // angle of pi / 2 -const RZ_PI2_11: Complex64 = Complex64::new(FRAC_1_SQRT_2, -FRAC_1_SQRT_2); -const RZ_PI2_00: Complex64 = Complex64::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2); +const RZ_PI2_11: Complex64 = c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2); +const RZ_PI2_00: Complex64 = c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2); /// This method implements the decomposition given in equation (3) in /// https://arxiv.org/pdf/quant-ph/0410066.pdf. @@ -48,10 +48,10 @@ fn demultiplex_single_uc( let x11 = x[[0, 0]] / det_x.sqrt(); let phi = det_x.arg(); - let r1 = (Complex64::new(0., 1.) / 2. * (PI2 - phi / 2. - x11.arg())).exp(); - let r2 = (Complex64::new(0., 1.) / 2. * (PI2 - phi / 2. + x11.arg() + PI)).exp(); + let r1 = (IM / 2. * (PI / 2. - phi / 2. - x11.arg())).exp(); + let r2 = (IM / 2. * (PI / 2. - phi / 2. + x11.arg() + PI)).exp(); - let r = array![[r1, Complex64::new(0., 0.)], [Complex64::new(0., 0.), r2],]; + let r = array![[r1, C_ZERO], [C_ZERO, r2],]; let decomp = r .dot(&x) @@ -67,7 +67,7 @@ fn demultiplex_single_uc( // If d is not equal to diag(i,-i), then we put it into this "standard" form // (see eq. (13) in https://arxiv.org/pdf/quant-ph/0410066.pdf) by interchanging // the eigenvalues and eigenvectors - if (diag[0] + Complex64::new(0., 1.)).abs() < EPS { + if (diag[0] + IM).abs() < EPS { diag = diag.slice(s![..;-1]).to_owned(); u = u.slice(s![.., ..;-1]).to_owned(); } diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 2a3fcdf8828..2f085ea79c0 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -10,22 +10,16 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use num_complex::Complex64; use std::f64::consts::FRAC_1_SQRT_2; -// num-complex exposes an equivalent function but it's not a const function -// so it's not compatible with static definitions. This is a const func and -// just reduces the amount of typing we need. -#[inline(always)] -const fn c64(re: f64, im: f64) -> Complex64 { - Complex64::new(re, im) -} +use crate::util::{ + c64, GateArray0Q, GateArray1Q, GateArray2Q, GateArray3Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM, +}; -pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = - [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(1., 0.)]]; +pub static ONE_QUBIT_IDENTITY: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_ONE]]; #[inline] -pub fn r_gate(theta: f64, phi: f64) -> [[Complex64; 2]; 2] { +pub fn r_gate(theta: f64, phi: f64) -> GateArray1Q { let half_theta = theta / 2.; let cost = c64(half_theta.cos(), 0.); let sint = half_theta.sin(); @@ -38,7 +32,7 @@ pub fn r_gate(theta: f64, phi: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { +pub fn rx_gate(theta: f64) -> GateArray1Q { let half_theta = theta / 2.; let cos = c64(half_theta.cos(), 0.); let isin = c64(0., -half_theta.sin()); @@ -46,7 +40,7 @@ pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn ry_gate(theta: f64) -> [[Complex64; 2]; 2] { +pub fn ry_gate(theta: f64) -> GateArray1Q { let half_theta = theta / 2.; let cos = c64(half_theta.cos(), 0.); let sin = c64(half_theta.sin(), 0.); @@ -54,213 +48,150 @@ pub fn ry_gate(theta: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] { +pub fn rz_gate(theta: f64) -> GateArray1Q { let ilam2 = c64(0., 0.5 * theta); - [[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]] + [[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } -pub static H_GATE: [[Complex64; 2]; 2] = [ +pub static H_GATE: GateArray1Q = [ [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], ]; -pub static CX_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], +pub static CX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], ]; -pub static SX_GATE: [[Complex64; 2]; 2] = [ +pub static SX_GATE: GateArray1Q = [ [c64(0.5, 0.5), c64(0.5, -0.5)], [c64(0.5, -0.5), c64(0.5, 0.5)], ]; -pub static SXDG_GATE: [[Complex64; 2]; 2] = [ +pub static SXDG_GATE: GateArray1Q = [ [c64(0.5, -0.5), c64(0.5, 0.5)], [c64(0.5, 0.5), c64(0.5, -0.5)], ]; -pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; +pub static X_GATE: GateArray1Q = [[C_ZERO, C_ONE], [C_ONE, C_ZERO]]; -pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; +pub static Z_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_M_ONE]]; -pub static Y_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; +pub static Y_GATE: GateArray1Q = [[C_ZERO, M_IM], [IM, C_ZERO]]; -pub static CZ_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(-1., 0.)], +pub static CZ_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_M_ONE], ]; -pub static CY_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(0., -1.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)], +pub static CY_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, M_IM], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, IM, C_ZERO, C_ZERO], ]; -pub static CCX_GATE: [[Complex64; 8]; 8] = [ +pub static CCX_GATE: GateArray3Q = [ [ - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), + C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, ], [ - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, ], [ - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, ], [ - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, ], [ - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, ], [ - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, ], [ - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, ], [ - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(1., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, ], ]; -pub static ECR_GATE: [[Complex64; 4]; 4] = [ +pub static ECR_GATE: GateArray2Q = [ [ - c64(0., 0.), + C_ZERO, c64(FRAC_1_SQRT_2, 0.), - c64(0., 0.), + C_ZERO, c64(0., FRAC_1_SQRT_2), ], [ c64(FRAC_1_SQRT_2, 0.), - c64(0., 0.), + C_ZERO, c64(0., -FRAC_1_SQRT_2), - c64(0., 0.), + C_ZERO, ], [ - c64(0., 0.), + C_ZERO, c64(0., FRAC_1_SQRT_2), - c64(0., 0.), + C_ZERO, c64(FRAC_1_SQRT_2, 0.), ], [ c64(0., -FRAC_1_SQRT_2), - c64(0., 0.), + C_ZERO, c64(FRAC_1_SQRT_2, 0.), - c64(0., 0.), + C_ZERO, ], ]; -pub static SWAP_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], +pub static SWAP_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], ]; -pub static ISWAP_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 1.), c64(0., 0.)], - [c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], +pub static ISWAP_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, IM, C_ZERO], + [C_ZERO, IM, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], ]; -pub static S_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., 1.)]]; +pub static S_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, IM]]; -pub static SDG_GATE: [[Complex64; 2]; 2] = - [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., -1.)]]; +pub static SDG_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, M_IM]]; -pub static T_GATE: [[Complex64; 2]; 2] = [ - [c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)], -]; +pub static T_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)]]; -pub static TDG_GATE: [[Complex64; 2]; 2] = [ - [c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], +pub static TDG_GATE: GateArray1Q = [ + [C_ONE, C_ZERO], + [C_ZERO, c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], ]; -pub static DCX_GATE: [[Complex64; 4]; 4] = [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], - [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], +pub static DCX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], ]; #[inline] -pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { +pub fn global_phase_gate(theta: f64) -> GateArray0Q { [[c64(0., theta).exp()]] } #[inline] -pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] { - [ - [c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., lam).exp()], - ] +pub fn phase_gate(lam: f64) -> GateArray1Q { + [[C_ONE, C_ZERO], [C_ZERO, c64(0., lam).exp()]] } #[inline] -pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { +pub fn u_gate(theta: f64, phi: f64, lam: f64) -> GateArray1Q { let cos = (theta / 2.).cos(); let sin = (theta / 2.).sin(); [ @@ -270,37 +201,34 @@ pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { +pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { let cos = (theta / 2.).cos(); let sin = (theta / 2.).sin(); [ [ c64(cos, 0.), - c64(0., 0.), - c64(0., 0.), + C_ZERO, + C_ZERO, c64(0., -sin) * c64(0., -beta).exp(), ], - [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], [ c64(0., -sin) * c64(0., beta).exp(), - c64(0., 0.), - c64(0., 0.), + C_ZERO, + C_ZERO, c64(cos, 0.), ], ] } #[inline] -pub fn u1_gate(lam: f64) -> [[Complex64; 2]; 2] { - [ - [c64(1., 0.), c64(0., 0.)], - [c64(0., 0.), c64(0., lam).exp()], - ] +pub fn u1_gate(lam: f64) -> GateArray1Q { + [[C_ONE, C_ZERO], [C_ZERO, c64(0., lam).exp()]] } #[inline] -pub fn u2_gate(phi: f64, lam: f64) -> [[Complex64; 2]; 2] { +pub fn u2_gate(phi: f64, lam: f64) -> GateArray1Q { [ [ c64(FRAC_1_SQRT_2, 0.), @@ -314,7 +242,7 @@ pub fn u2_gate(phi: f64, lam: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { +pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> GateArray1Q { let cos = (theta / 2.).cos(); let sin = (theta / 2.).sin(); [ @@ -324,23 +252,23 @@ pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { } #[inline] -pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { +pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { let cos = (theta / 2.).cos(); let sin = (theta / 2.).sin(); [ - [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [C_ONE, C_ZERO, C_ZERO, C_ZERO], [ - c64(0., 0.), + C_ZERO, c64(cos, 0.), c64(0., -sin) * c64(0., -beta).exp(), - c64(0., 0.), + C_ZERO, ], [ - c64(0., 0.), + C_ZERO, c64(0., -sin) * c64(0., beta).exp(), c64(cos, 0.), - c64(0., 0.), + C_ZERO, ], - [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], ] } diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index d7f28591175..9fcaa36480c 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,6 +17,7 @@ pub mod gate_matrix; pub mod imports; pub mod operations; pub mod parameter_table; +pub mod util; mod bit_data; mod interner; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index e0e93726735..ff730744c80 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -24,9 +24,6 @@ use pyo3::prelude::*; use pyo3::{intern, IntoPy, Python}; use smallvec::smallvec; -const PI2: f64 = PI / 2.0; -const PI4: f64 = PI / 4.0; - /// Valid types for an operation field in a CircuitInstruction /// /// These are basically the types allowed in a QuantumCircuit @@ -563,7 +560,11 @@ impl Operation for StandardGate { 1, [( Self::UGate, - smallvec![Param::Float(PI), Param::Float(PI2), Param::Float(PI2),], + smallvec![ + Param::Float(PI), + Param::Float(PI / 2.), + Param::Float(PI / 2.), + ], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -732,7 +733,7 @@ impl Operation for StandardGate { 1, [( Self::UGate, - smallvec![Param::Float(PI2), Param::Float(0.), Param::Float(PI)], + smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -763,7 +764,7 @@ impl Operation for StandardGate { 1, [( Self::PhaseGate, - smallvec![Param::Float(PI2)], + smallvec![Param::Float(PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -793,7 +794,7 @@ impl Operation for StandardGate { 1, [( Self::PhaseGate, - smallvec![Param::Float(-PI2)], + smallvec![Param::Float(-PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -823,7 +824,7 @@ impl Operation for StandardGate { 1, [( Self::PhaseGate, - smallvec![Param::Float(PI4)], + smallvec![Param::Float(PI / 4.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -853,7 +854,7 @@ impl Operation for StandardGate { 1, [( Self::PhaseGate, - smallvec![Param::Float(-PI4)], + smallvec![Param::Float(-PI / 4.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -895,9 +896,9 @@ impl Operation for StandardGate { smallvec![multiply_param(beta, -1.0, py)], q1.clone(), ), - (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q0.clone()), (Self::SXGate, smallvec![], q0.clone()), - (Self::RZGate, smallvec![Param::Float(PI2)], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q0.clone()), (Self::SGate, smallvec![], q1.clone()), (Self::CXGate, smallvec![], q0_1.clone()), ( @@ -912,9 +913,9 @@ impl Operation for StandardGate { ), (Self::CXGate, smallvec![], q0_1), (Self::SdgGate, smallvec![], q1.clone()), - (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q0.clone()), (Self::SXdgGate, smallvec![], q0.clone()), - (Self::RZGate, smallvec![Param::Float(PI2)], q0), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q0), (Self::RZGate, smallvec![beta.clone()], q1), ], FLOAT_ZERO, @@ -934,9 +935,9 @@ impl Operation for StandardGate { 2, [ (Self::RZGate, smallvec![beta.clone()], q0.clone()), - (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q1.clone()), (Self::SXGate, smallvec![], q1.clone()), - (Self::RZGate, smallvec![Param::Float(PI2)], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q1.clone()), (Self::SGate, smallvec![], q0.clone()), (Self::CXGate, smallvec![], q1_0.clone()), ( @@ -951,9 +952,9 @@ impl Operation for StandardGate { ), (Self::CXGate, smallvec![], q1_0), (Self::SdgGate, smallvec![], q0.clone()), - (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q1.clone()), (Self::SXdgGate, smallvec![], q1.clone()), - (Self::RZGate, smallvec![Param::Float(PI2)], q1), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q1), (Self::RZGate, smallvec![multiply_param(beta, -1.0, py)], q0), ], FLOAT_ZERO, @@ -964,7 +965,7 @@ impl Operation for StandardGate { Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), Self::RGate => Python::with_gil(|py| -> Option { let theta_expr = clone_param(¶ms[0], py); - let phi_expr1 = add_param(¶ms[1], -PI2, py); + let phi_expr1 = add_param(¶ms[1], -PI / 2., py); let phi_expr2 = multiply_param(&phi_expr1, -1.0, py); let defparams = smallvec![theta_expr, phi_expr1, phi_expr2]; Some( diff --git a/crates/circuit/src/util.rs b/crates/circuit/src/util.rs new file mode 100644 index 00000000000..11562b0a48c --- /dev/null +++ b/crates/circuit/src/util.rs @@ -0,0 +1,48 @@ +// 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 num_complex::Complex64; + +// This is a very conservative version of an abbreviation for constructing new Complex64. +// A couple of alternatives to this function are +// `c64, V: Into>(re: T, im: V) -> Complex64` +// Disadvantages are: +// 1. Some people don't like that this allows things like `c64(1, 0)`. Presumably, +// they prefer a more explicit construction. +// 2. This will not work in `const` and `static` constructs. +// Another alternative is +// macro_rules! c64 { +// ($re: expr, $im: expr $(,)*) => { +// Complex64::new($re as f64, $im as f64) +// }; +// Advantages: This allows things like `c64!(1, 2.0)`, including in +// `static` and `const` constructs. +// Disadvantages: +// 1. Three characters `c64!` rather than two `c64`. +// 2. Some people prefer the opposite of the advantages, i.e. more explicitness. +/// Create a new [`Complex`] +#[inline(always)] +pub const fn c64(re: f64, im: f64) -> Complex64 { + Complex64::new(re, im) +} + +pub type GateArray0Q = [[Complex64; 1]; 1]; +pub type GateArray1Q = [[Complex64; 2]; 2]; +pub type GateArray2Q = [[Complex64; 4]; 4]; +pub type GateArray3Q = [[Complex64; 8]; 8]; + +// Use prefix `C_` to distinguish from real, for example +pub const C_ZERO: Complex64 = c64(0., 0.); +pub const C_ONE: Complex64 = c64(1., 0.); +pub const C_M_ONE: Complex64 = c64(-1., 0.); +pub const IM: Complex64 = c64(0., 1.); +pub const M_IM: Complex64 = c64(0., -1.); From 3adcd5d3dfa8f67cb4ebb37f99a0d388942602af Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 28 Jun 2024 14:36:32 +0100 Subject: [PATCH 149/159] Suppress nonsense `DeprecationWarning` caused by `unittest` (#12676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Suppress nonsense `DeprecationWarning` caused by `unittest` `unittest.TestCase.assertWarns` in context-manager form has an awkward habit of querying the `__warningregistry__` attribute on every module in existence. This interacts poorly with a Numpy 2 deprecation warning trigger for code that's attempting to import functions from modules that became private in Numpy 2, if a warning has previously been triggered out of `numpy.linalg._linalg`. This simply suppresses that particular warning from the test suite. * Refine filter * Pin Rustworkx to avoid buggy graphviz drawing * Update test/utils/base.py 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> --- constraints.txt | 4 ++++ test/utils/base.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/constraints.txt b/constraints.txt index 6681de226d9..8c561f52f0f 100644 --- a/constraints.txt +++ b/constraints.txt @@ -7,6 +7,10 @@ scipy<1.11; python_version<'3.12' # See https://github.com/Qiskit/qiskit/issues/12655 for current details. scipy==1.13.1; python_version=='3.12' +# Rustworkx 0.15.0 contains a bug that breaks graphviz-related tests. +# See https://github.com/Qiskit/rustworkx/pull/1229 for the fix. +rustworkx==0.14.2 + # z3-solver from 4.12.3 onwards upped the minimum macOS API version for its # wheels to 11.7. The Azure VM images contain pre-built CPythons, of which at # least CPython 3.8 was compiled for an older macOS, so does not match a diff --git a/test/utils/base.py b/test/utils/base.py index bebf0300885..ce9509709ba 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -204,6 +204,20 @@ def setUpClass(cls): warnings.filterwarnings("error", category=DeprecationWarning) warnings.filterwarnings("error", category=QiskitWarning) + # Numpy 2 made a few new modules private, and have warnings that trigger if you try to + # access attributes that _would_ have existed. Unfortunately, Python's `warnings` module + # adds a field called `__warningregistry__` to any module that triggers a warning, and + # `unittest.TestCase.assertWarns` then queries said fields on all existing modules. On + # macOS ARM, we see some (we think harmless) warnings come out of `numpy.linalg._linalg` (a + # now-private module) during transpilation, which means that subsequent `assertWarns` calls + # can spuriously trick Numpy into sending out a nonsense `DeprecationWarning`. + # Tracking issue: https://github.com/Qiskit/qiskit/issues/12679 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*numpy\.(\w+\.)*__warningregistry__", + ) + # We only use pandas transitively through seaborn, so it's their responsibility to mark if # their use of pandas would be a problem. warnings.filterwarnings( From 9b0a5849f637a25f938fcd118352cdfeae1a58e5 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 28 Jun 2024 17:34:30 +0200 Subject: [PATCH 150/159] Port CRX/Y/Z gates to Rust (#12648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v0 of CR-Pauli gates * fix inevitable matrix typos * update multiply_param and prepare for U1/2/3 PR * fix num params/qubits * cct methods to append rust gates --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/gate_matrix.rs | 37 +++++++ crates/circuit/src/imports.rs | 6 +- crates/circuit/src/operations.rs | 110 ++++++++++++++++++-- qiskit/circuit/library/standard_gates/rx.py | 2 + qiskit/circuit/library/standard_gates/ry.py | 2 + qiskit/circuit/library/standard_gates/rz.py | 2 + qiskit/circuit/quantumcircuit.py | 18 ++++ 7 files changed, 168 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 2f085ea79c0..074b1c2ac68 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -53,6 +53,43 @@ pub fn rz_gate(theta: f64) -> GateArray1Q { [[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } +#[inline] +pub fn crx_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., half_theta.sin()); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -isin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, -isin, C_ZERO, cos], + ] +} + +#[inline] +pub fn cry_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -sin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, sin, C_ZERO, cos], + ] +} + +#[inline] +pub fn crz_gate(theta: f64) -> GateArray2Q { + let i_half_theta = c64(0., theta / 2.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], + ] +} + pub static H_GATE: GateArray1Q = [ [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index bf06685ba53..530e635c94f 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -151,11 +151,11 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // U3Gate = 28 ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], // CRXGate = 29 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rx", "CRXGate"], // CRYGate = 30 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.ry", "CRYGate"], // CRZGate = 31 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rz", "CRZGate"], // RGate 32 ["qiskit.circuit.library.standard_gates.r", "RGate"], // CHGate = 33 diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index ff730744c80..85192b63dbd 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -235,8 +235,8 @@ pub enum StandardGate { static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 - 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 - 34, 34, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, // 20-29 + 2, 2, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 2, 2, 34, 34, 34, 2, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -245,8 +245,8 @@ static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 - 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 - 34, 34, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 0, 0, 0, 0, 2, 2, 1, 2, 3, 1, // 20-29 + 1, 1, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 1, 3, 34, 34, 34, 0, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -422,6 +422,18 @@ impl Operation for StandardGate { [Param::Float(theta)] => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), _ => None, }, + Self::CRXGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::crx_gate(*theta)).to_owned()), + _ => None, + }, + Self::CRYGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::cry_gate(*theta)).to_owned()), + _ => None, + }, + Self::CRZGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::crz_gate(*theta)).to_owned()), + _ => None, + }, Self::ECRGate => match params { [] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()), _ => None, @@ -510,7 +522,6 @@ impl Operation for StandardGate { } _ => None, }, - Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), Self::RGate => match params { [Param::Float(theta), Param::Float(phi)] => { Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) @@ -673,6 +684,94 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::CRXGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::PhaseGate, + smallvec![Param::Float(PI / 2.)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::UGate, + smallvec![ + multiply_param(theta, -0.5, py), + Param::Float(0.0), + Param::Float(0.0) + ], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::UGate, + smallvec![ + multiply_param(theta, 0.5, py), + Param::Float(-PI / 2.), + Param::Float(0.0) + ], + smallvec![Qubit(1)], + ), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), + Self::CRYGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RYGate, + smallvec![multiply_param(theta, 0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), + Self::CRZGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RZGate, + smallvec![multiply_param(theta, 0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::RZGate, + smallvec![multiply_param(theta, -0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), Self::ECRGate => todo!("Add when we have RZX"), Self::SwapGate => Python::with_gil(|py| -> Option { Some( @@ -962,7 +1061,6 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), Self::RGate => Python::with_gil(|py| -> Option { let theta_expr = clone_param(¶ms[0], py); let phi_expr1 = add_param(¶ms[1], -PI / 2., py); diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index 5579f9d3707..cb851a740d2 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -199,6 +199,8 @@ class CRXGate(ControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CRXGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index e27398cc296..b60b34ffde6 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -198,6 +198,8 @@ class CRYGate(ControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CRYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index e8ee0f97603..78cf20efa5c 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -216,6 +216,8 @@ class CRZGate(ControlledGate): phase difference. """ + _standard_gate = StandardGate.CRZGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 485591a8a3b..7b8fe6e031f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4774,6 +4774,12 @@ def crx( """ from .library.standard_gates.rx import CRXGate + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRXGate, [theta], [control_qubit, target_qubit], None, label=label + ) + return self.append( CRXGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4843,6 +4849,12 @@ def cry( """ from .library.standard_gates.ry import CRYGate + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRYGate, [theta], [control_qubit, target_qubit], None, label=label + ) + return self.append( CRYGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4909,6 +4921,12 @@ def crz( """ from .library.standard_gates.rz import CRZGate + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRZGate, [theta], [control_qubit, target_qubit], None, label=label + ) + return self.append( CRZGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], From 24ee7c69b5acf9c0b10030c1f180299e6df5a4c7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 28 Jun 2024 12:03:37 -0400 Subject: [PATCH 151/159] Fix clippy warnings on latest stable rust (#12675) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Rust 1.79.0 several new clippy rules were added and/or enabled by default. This was causing some new issues to be flagged when building qiskit with the this release of Rust. This commit fixes these issues flagged by clippy. Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/accelerate/src/dense_layout.rs | 2 +- crates/accelerate/src/nlayout.rs | 6 +++--- crates/accelerate/src/stochastic_swap.rs | 10 +++++----- crates/circuit/src/circuit_instruction.rs | 5 +---- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/accelerate/src/dense_layout.rs b/crates/accelerate/src/dense_layout.rs index 901a906d9c8..9529742d7e6 100644 --- a/crates/accelerate/src/dense_layout.rs +++ b/crates/accelerate/src/dense_layout.rs @@ -197,7 +197,7 @@ pub fn best_subset_inner( SubsetResult { count: 0, map: Vec::new(), - error: std::f64::INFINITY, + error: f64::INFINITY, subgraph: Vec::new(), } }; diff --git a/crates/accelerate/src/nlayout.rs b/crates/accelerate/src/nlayout.rs index 1a0b73b25fe..b3709d2804b 100644 --- a/crates/accelerate/src/nlayout.rs +++ b/crates/accelerate/src/nlayout.rs @@ -107,8 +107,8 @@ impl NLayout { physical_qubits: usize, ) -> Self { let mut res = NLayout { - virt_to_phys: vec![PhysicalQubit(std::u32::MAX); virtual_qubits], - phys_to_virt: vec![VirtualQubit(std::u32::MAX); physical_qubits], + virt_to_phys: vec![PhysicalQubit(u32::MAX); virtual_qubits], + phys_to_virt: vec![VirtualQubit(u32::MAX); physical_qubits], }; for (virt, phys) in qubit_indices { res.virt_to_phys[virt.index()] = phys; @@ -184,7 +184,7 @@ impl NLayout { #[staticmethod] pub fn from_virtual_to_physical(virt_to_phys: Vec) -> PyResult { - let mut phys_to_virt = vec![VirtualQubit(std::u32::MAX); virt_to_phys.len()]; + let mut phys_to_virt = vec![VirtualQubit(u32::MAX); virt_to_phys.len()]; for (virt, phys) in virt_to_phys.iter().enumerate() { phys_to_virt[phys.index()] = VirtualQubit(virt.try_into()?); } diff --git a/crates/accelerate/src/stochastic_swap.rs b/crates/accelerate/src/stochastic_swap.rs index bc13325d8d9..d4e3890b9cc 100644 --- a/crates/accelerate/src/stochastic_swap.rs +++ b/crates/accelerate/src/stochastic_swap.rs @@ -112,10 +112,10 @@ fn swap_trial( let mut new_cost: f64; let mut dist: f64; - let mut optimal_start = PhysicalQubit::new(std::u32::MAX); - let mut optimal_end = PhysicalQubit::new(std::u32::MAX); - let mut optimal_start_qubit = VirtualQubit::new(std::u32::MAX); - let mut optimal_end_qubit = VirtualQubit::new(std::u32::MAX); + let mut optimal_start = PhysicalQubit::new(u32::MAX); + let mut optimal_end = PhysicalQubit::new(u32::MAX); + let mut optimal_start_qubit = VirtualQubit::new(u32::MAX); + let mut optimal_end_qubit = VirtualQubit::new(u32::MAX); let mut scale = Array2::zeros((num_qubits, num_qubits)); @@ -270,7 +270,7 @@ pub fn swap_trials( // unless force threads is set. let run_in_parallel = getenv_use_multiple_threads(); - let mut best_depth = std::usize::MAX; + let mut best_depth = usize::MAX; let mut best_edges: Option = None; let mut best_layout: Option = None; if run_in_parallel { diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 781a776c156..74302b526d5 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -825,10 +825,7 @@ pub(crate) fn convert_py_to_operation_type( }; let op_type: Bound = raw_op_type.into_bound(py); let mut standard: Option = match op_type.getattr(attr) { - Ok(stdgate) => match stdgate.extract().ok() { - Some(gate) => gate, - None => None, - }, + Ok(stdgate) => stdgate.extract().ok().unwrap_or_default(), Err(_) => None, }; // If the input instruction is a standard gate and a singleton instance From 3af991856ff3cc6925093224d5eecf6798be5265 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:14:30 +0000 Subject: [PATCH 152/159] Bump num-bigint from 0.4.5 to 0.4.6 (#12681) Bumps [num-bigint](https://github.com/rust-num/num-bigint) from 0.4.5 to 0.4.6. - [Changelog](https://github.com/rust-num/num-bigint/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-bigint/compare/num-bigint-0.4.5...num-bigint-0.4.6) --- updated-dependencies: - dependency-name: num-bigint 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 454823748e8..68a3d321406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", From 44fe59b04045c52f9031a5e8d91f8d5990f7d553 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 28 Jun 2024 20:39:56 +0100 Subject: [PATCH 153/159] Relax CI constraint on Rustworkx 0.15.0 (#12690) The release of Rustworkx 0.15.1 fixes the bug that was previously blocking CI. --- constraints.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/constraints.txt b/constraints.txt index 8c561f52f0f..6681de226d9 100644 --- a/constraints.txt +++ b/constraints.txt @@ -7,10 +7,6 @@ scipy<1.11; python_version<'3.12' # See https://github.com/Qiskit/qiskit/issues/12655 for current details. scipy==1.13.1; python_version=='3.12' -# Rustworkx 0.15.0 contains a bug that breaks graphviz-related tests. -# See https://github.com/Qiskit/rustworkx/pull/1229 for the fix. -rustworkx==0.14.2 - # z3-solver from 4.12.3 onwards upped the minimum macOS API version for its # wheels to 11.7. The Azure VM images contain pre-built CPythons, of which at # least CPython 3.8 was compiled for an older macOS, so does not match a From e9208a6339becd95b3e5e3a28e593c61d71aeb54 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:18:17 +0300 Subject: [PATCH 154/159] binary matrices utils in rust (#12456) * gaussian elimination in rust * handle lint errors * replace python function by rust function for gauss elimination * change matrix elements type from bool to i8 * add parallelization in row operations * update matrices in place * change matrix type in rust code to bool * handle type in python code * update filter following review * remove parallelization using rayon * move _gauss_elimination_with_perm to rust * fix fmt error * simplify _gauss_elimination function * update _compute_rank_after_gauss_elim to rust * update _row_op and _col_op * transfer _row_op and _col_op from python to rust * fix code due to failing tests * minor update of types * move calc_inverse_matrix to rust, add _binary_matmul in rust * fix failing tests, by changing mat type from int to bool * update rust docstrings * add function _add_row_or_col to rust code * improve binary_matmul * proper error handling * unified format of function names * move compute_rank from python to rust, update errors * update type of mat in compute_rank * move random_invertible_binary_matrix and check_invertible_binary_matrix to rust * Updating HighLevelSynthesis tests that depend on the specific random number * Updating LinearSynthesis tests to pass seeds * Updating tests in test_linear_function * changing the matrix type in random dyhedral to be a matrix of ints rather than bools * updating cx_cz synthesis tests * updating clifford tests * remove unused imports * add option seed=None * enhance rust docs * add release notes * remove unnecessary copy in python * another copy fix * another copy fix * update rust docstrings * update release notes --------- Co-authored-by: AlexanderIvrii --- crates/accelerate/src/synthesis/linear/mod.rs | 190 +++++++++++++++++ .../accelerate/src/synthesis/linear/utils.rs | 200 ++++++++++++++++++ crates/accelerate/src/synthesis/mod.rs | 2 + qiskit/__init__.py | 1 + .../quantum_info/operators/dihedral/random.py | 3 +- .../clifford/clifford_decompose_layers.py | 28 +-- qiskit/synthesis/linear/__init__.py | 1 + qiskit/synthesis/linear/linear_depth_lnn.py | 14 +- .../synthesis/linear/linear_matrix_utils.py | 174 ++------------- .../stabilizer/stabilizer_decompose.py | 2 +- .../passes/synthesis/high_level_synthesis.py | 4 +- ...ry-matrix-utils-rust-c48b5577749c34ab.yaml | 8 + .../circuit/library/test_linear_function.py | 15 +- .../operators/symplectic/test_clifford.py | 6 +- test/python/synthesis/test_cx_cz_synthesis.py | 5 +- .../python/synthesis/test_linear_synthesis.py | 9 +- .../transpiler/test_high_level_synthesis.py | 22 +- 17 files changed, 474 insertions(+), 210 deletions(-) create mode 100644 crates/accelerate/src/synthesis/linear/mod.rs create mode 100644 crates/accelerate/src/synthesis/linear/utils.rs create mode 100644 releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs new file mode 100644 index 00000000000..2fa158ea761 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -0,0 +1,190 @@ +// 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 crate::QiskitError; +use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; +use pyo3::prelude::*; + +mod utils; + +#[pyfunction] +#[pyo3(signature = (mat, ncols=None, full_elim=false))] +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// Modifies the matrix mat in-place, and returns the permutation perm that was done +/// on the rows during the process. perm[0 : rank] represents the indices of linearly +/// independent rows in the original matrix. +/// Args: +/// mat: a boolean matrix with n rows and m columns +/// ncols: the number of columns for the gaussian elimination, +/// if ncols=None, then the elimination is done over all the columns +/// full_elim: whether to do a full elimination, or partial (upper triangular form) +/// Returns: +/// perm: the permutation perm that was done on the rows during the process +fn gauss_elimination_with_perm( + py: Python, + mut mat: PyReadwriteArray2, + ncols: Option, + full_elim: Option, +) -> PyResult { + let matmut = mat.as_array_mut(); + let perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim); + Ok(perm.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat, ncols=None, full_elim=false))] +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// This function modifies the input matrix in-place. +/// Args: +/// mat: a boolean matrix with n rows and m columns +/// ncols: the number of columns for the gaussian elimination, +/// if ncols=None, then the elimination is done over all the columns +/// full_elim: whether to do a full elimination, or partial (upper triangular form) +fn gauss_elimination( + mut mat: PyReadwriteArray2, + ncols: Option, + full_elim: Option, +) { + let matmut = mat.as_array_mut(); + let _perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim); +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Given a boolean matrix mat after Gaussian elimination, computes its rank +/// (i.e. simply the number of nonzero rows) +/// Args: +/// mat: a boolean matrix after gaussian elimination +/// Returns: +/// rank: the rank of the matrix +fn compute_rank_after_gauss_elim(py: Python, mat: PyReadonlyArray2) -> PyResult { + let view = mat.as_array(); + let rank = utils::compute_rank_after_gauss_elim_inner(view); + Ok(rank.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Given a boolean matrix mat computes its rank +/// Args: +/// mat: a boolean matrix +/// Returns: +/// rank: the rank of the matrix +fn compute_rank(py: Python, mat: PyReadonlyArray2) -> PyResult { + let rank = utils::compute_rank_inner(mat.as_array()); + Ok(rank.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat, verify=false))] +/// Given a boolean matrix mat, tries to calculate its inverse matrix +/// Args: +/// mat: a boolean square matrix. +/// verify: if True asserts that the multiplication of mat and its inverse is the identity matrix. +/// Returns: +/// the inverse matrix. +/// Raises: +/// QiskitError: if the matrix is not square or not invertible. +pub fn calc_inverse_matrix( + py: Python, + mat: PyReadonlyArray2, + verify: Option, +) -> PyResult>> { + let view = mat.as_array(); + let invmat = + utils::calc_inverse_matrix_inner(view, verify.is_some()).map_err(QiskitError::new_err)?; + Ok(invmat.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat1, mat2))] +/// Binary matrix multiplication +/// Args: +/// mat1: a boolean matrix +/// mat2: a boolean matrix +/// Returns: +/// a boolean matrix which is the multiplication of mat1 and mat2 +/// Raises: +/// QiskitError: if the dimensions of mat1 and mat2 do not match +pub fn binary_matmul( + py: Python, + mat1: PyReadonlyArray2, + mat2: PyReadonlyArray2, +) -> PyResult>> { + let view1 = mat1.as_array(); + let view2 = mat2.as_array(); + let result = utils::binary_matmul_inner(view1, view2).map_err(QiskitError::new_err)?; + Ok(result.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat, ctrl, trgt))] +/// Perform ROW operation on a matrix mat +fn row_op(mut mat: PyReadwriteArray2, ctrl: usize, trgt: usize) { + let matmut = mat.as_array_mut(); + utils::_add_row_or_col(matmut, &false, ctrl, trgt) +} + +#[pyfunction] +#[pyo3(signature = (mat, ctrl, trgt))] +/// Perform COL operation on a matrix mat (in the inverse direction) +fn col_op(mut mat: PyReadwriteArray2, ctrl: usize, trgt: usize) { + let matmut = mat.as_array_mut(); + utils::_add_row_or_col(matmut, &true, trgt, ctrl) +} + +#[pyfunction] +#[pyo3(signature = (num_qubits, seed=None))] +/// Generate a random invertible n x n binary matrix. +/// Args: +/// num_qubits: the matrix size. +/// seed: a random seed. +/// Returns: +/// np.ndarray: A random invertible binary matrix of size num_qubits. +fn random_invertible_binary_matrix( + py: Python, + num_qubits: usize, + seed: Option, +) -> PyResult>> { + let matrix = utils::random_invertible_binary_matrix_inner(num_qubits, seed); + Ok(matrix.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Check that a binary matrix is invertible. +/// Args: +/// mat: a binary matrix. +/// Returns: +/// bool: True if mat in invertible and False otherwise. +fn check_invertible_binary_matrix(py: Python, mat: PyReadonlyArray2) -> PyResult { + let view = mat.as_array(); + let out = utils::check_invertible_binary_matrix_inner(view); + Ok(out.to_object(py)) +} + +#[pymodule] +pub fn linear(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(gauss_elimination_with_perm))?; + m.add_wrapped(wrap_pyfunction!(gauss_elimination))?; + m.add_wrapped(wrap_pyfunction!(compute_rank_after_gauss_elim))?; + m.add_wrapped(wrap_pyfunction!(compute_rank))?; + m.add_wrapped(wrap_pyfunction!(calc_inverse_matrix))?; + m.add_wrapped(wrap_pyfunction!(row_op))?; + m.add_wrapped(wrap_pyfunction!(col_op))?; + m.add_wrapped(wrap_pyfunction!(binary_matmul))?; + m.add_wrapped(wrap_pyfunction!(random_invertible_binary_matrix))?; + m.add_wrapped(wrap_pyfunction!(check_invertible_binary_matrix))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/linear/utils.rs b/crates/accelerate/src/synthesis/linear/utils.rs new file mode 100644 index 00000000000..b4dbf499308 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/utils.rs @@ -0,0 +1,200 @@ +// 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 ndarray::{concatenate, s, Array2, ArrayView2, ArrayViewMut2, Axis}; +use rand::{Rng, SeedableRng}; +use rand_pcg::Pcg64Mcg; + +/// Binary matrix multiplication +pub fn binary_matmul_inner( + mat1: ArrayView2, + mat2: ArrayView2, +) -> Result, String> { + let n1_rows = mat1.nrows(); + let n1_cols = mat1.ncols(); + let n2_rows = mat2.nrows(); + let n2_cols = mat2.ncols(); + if n1_cols != n2_rows { + return Err(format!( + "Cannot multiply matrices with inappropriate dimensions {}, {}", + n1_cols, n2_rows + )); + } + + Ok(Array2::from_shape_fn((n1_rows, n2_cols), |(i, j)| { + (0..n2_rows) + .map(|k| mat1[[i, k]] & mat2[[k, j]]) + .fold(false, |acc, v| acc ^ v) + })) +} + +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// Returns the matrix mat, and the permutation perm that was done on the rows during the process. +/// perm[0 : rank] represents the indices of linearly independent rows in the original matrix. +pub fn gauss_elimination_with_perm_inner( + mut mat: ArrayViewMut2, + ncols: Option, + full_elim: Option, +) -> Vec { + let (m, mut n) = (mat.nrows(), mat.ncols()); // no. of rows and columns + if let Some(ncols_val) = ncols { + n = usize::min(n, ncols_val); // no. of active columns + } + let mut perm: Vec = Vec::from_iter(0..m); + + let mut r = 0; // current rank + let k = 0; // current pivot column + let mut new_k = 0; + while (r < m) && (k < n) { + let mut is_non_zero = false; + let mut new_r = r; + for j in k..n { + new_k = k; + for i in r..m { + if mat[(i, j)] { + is_non_zero = true; + new_k = j; + new_r = i; + break; + } + } + if is_non_zero { + break; + } + } + if !is_non_zero { + return perm; // A is in the canonical form + } + + if new_r != r { + let temp_r = mat.slice_mut(s![r, ..]).to_owned(); + let temp_new_r = mat.slice_mut(s![new_r, ..]).to_owned(); + mat.slice_mut(s![r, ..]).assign(&temp_new_r); + mat.slice_mut(s![new_r, ..]).assign(&temp_r); + perm.swap(r, new_r); + } + + // Copy source row to avoid trying multiple borrows at once + let row0 = mat.row(r).to_owned(); + mat.axis_iter_mut(Axis(0)) + .enumerate() + .filter(|(i, row)| { + (full_elim == Some(true) && (*i < r) && row[new_k]) + || (*i > r && *i < m && row[new_k]) + }) + .for_each(|(_i, mut row)| { + row.zip_mut_with(&row0, |x, &y| *x ^= y); + }); + + r += 1; + } + perm +} + +/// Given a boolean matrix A after Gaussian elimination, computes its rank +/// (i.e. simply the number of nonzero rows) +pub fn compute_rank_after_gauss_elim_inner(mat: ArrayView2) -> usize { + let rank: usize = mat + .axis_iter(Axis(0)) + .map(|row| row.fold(false, |out, val| out | *val) as usize) + .sum(); + rank +} + +/// Given a boolean matrix mat computes its rank +pub fn compute_rank_inner(mat: ArrayView2) -> usize { + let mut temp_mat = mat.to_owned(); + gauss_elimination_with_perm_inner(temp_mat.view_mut(), None, Some(false)); + let rank = compute_rank_after_gauss_elim_inner(temp_mat.view()); + rank +} + +/// Given a square boolean matrix mat, tries to compute its inverse. +pub fn calc_inverse_matrix_inner( + mat: ArrayView2, + verify: bool, +) -> Result, String> { + if mat.shape()[0] != mat.shape()[1] { + return Err("Matrix to invert is a non-square matrix.".to_string()); + } + let n = mat.shape()[0]; + + // concatenate the matrix and identity + let identity_matrix: Array2 = Array2::from_shape_fn((n, n), |(i, j)| i == j); + let mut mat1 = concatenate(Axis(1), &[mat.view(), identity_matrix.view()]).unwrap(); + + gauss_elimination_with_perm_inner(mat1.view_mut(), None, Some(true)); + + let r = compute_rank_after_gauss_elim_inner(mat1.slice(s![.., 0..n])); + if r < n { + return Err("The matrix is not invertible.".to_string()); + } + + let invmat = mat1.slice(s![.., n..2 * n]).to_owned(); + + if verify { + let mat2 = binary_matmul_inner(mat, (&invmat).into())?; + let identity_matrix: Array2 = Array2::from_shape_fn((n, n), |(i, j)| i == j); + if mat2.ne(&identity_matrix) { + return Err("The inverse matrix is not correct.".to_string()); + } + } + + Ok(invmat) +} + +/// Mutate a matrix inplace by adding the value of the ``ctrl`` row to the +/// ``target`` row. If ``add_cols`` is true, add columns instead of rows. +pub fn _add_row_or_col(mut mat: ArrayViewMut2, add_cols: &bool, ctrl: usize, trgt: usize) { + // get the two rows (or columns) + let info = if *add_cols { + (s![.., ctrl], s![.., trgt]) + } else { + (s![ctrl, ..], s![trgt, ..]) + }; + let (row0, mut row1) = mat.multi_slice_mut(info); + + // add them inplace + row1.zip_mut_with(&row0, |x, &y| *x ^= y); +} + +/// Generate a random invertible n x n binary matrix. +pub fn random_invertible_binary_matrix_inner(num_qubits: usize, seed: Option) -> Array2 { + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + + let mut matrix = Array2::from_elem((num_qubits, num_qubits), false); + + loop { + for value in matrix.iter_mut() { + *value = rng.gen_bool(0.5); + } + + let rank = compute_rank_inner(matrix.view()); + if rank == num_qubits { + break; + } + } + matrix +} + +/// Check that a binary matrix is invertible. +pub fn check_invertible_binary_matrix_inner(mat: ArrayView2) -> bool { + if mat.nrows() != mat.ncols() { + return false; + } + let rank = compute_rank_inner(mat); + rank == mat.nrows() +} diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index f1a72045921..db28751437f 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/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 linear; mod permutation; use pyo3::prelude::*; @@ -18,5 +19,6 @@ use pyo3::wrap_pymodule; #[pymodule] pub fn synthesis(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + m.add_wrapped(wrap_pymodule!(linear::linear))?; Ok(()) } diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 5b850565442..aca555da8cb 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -81,6 +81,7 @@ sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation +sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/quantum_info/operators/dihedral/random.py b/qiskit/quantum_info/operators/dihedral/random.py index 4331d618d73..8223f87e9a1 100644 --- a/qiskit/quantum_info/operators/dihedral/random.py +++ b/qiskit/quantum_info/operators/dihedral/random.py @@ -53,7 +53,8 @@ def random_cnotdihedral(num_qubits, seed=None): random_invertible_binary_matrix, ) - linear = random_invertible_binary_matrix(num_qubits, seed=rng) + seed = rng.integers(100000, size=1, dtype=np.uint64)[0] + linear = random_invertible_binary_matrix(num_qubits, seed=seed).astype(int, copy=False) elem.linear = linear # Random shift diff --git a/qiskit/synthesis/clifford/clifford_decompose_layers.py b/qiskit/synthesis/clifford/clifford_decompose_layers.py index 21bea89b657..8b745823dc1 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_layers.py +++ b/qiskit/synthesis/clifford/clifford_decompose_layers.py @@ -33,9 +33,10 @@ from qiskit.synthesis.linear_phase import synth_cz_depth_line_mr, synth_cx_cz_depth_line_my from qiskit.synthesis.linear.linear_matrix_utils import ( calc_inverse_matrix, - _compute_rank, - _gauss_elimination, - _gauss_elimination_with_perm, + compute_rank, + gauss_elimination, + gauss_elimination_with_perm, + binary_matmul, ) @@ -203,24 +204,25 @@ def _create_graph_state(cliff, validate=False): """ num_qubits = cliff.num_qubits - rank = _compute_rank(cliff.stab_x) + rank = compute_rank(np.asarray(cliff.stab_x, dtype=bool)) H1_circ = QuantumCircuit(num_qubits, name="H1") cliffh = cliff.copy() if rank < num_qubits: stab = cliff.stab[:, :-1] - stab = _gauss_elimination(stab, num_qubits) + stab = stab.astype(bool, copy=True) + gauss_elimination(stab, num_qubits) Cmat = stab[rank:num_qubits, num_qubits:] Cmat = np.transpose(Cmat) - Cmat, perm = _gauss_elimination_with_perm(Cmat) + perm = gauss_elimination_with_perm(Cmat) perm = perm[0 : num_qubits - rank] # validate that the output matrix has the same rank if validate: - if _compute_rank(Cmat) != num_qubits - rank: + if compute_rank(Cmat) != num_qubits - rank: raise QiskitError("The matrix Cmat after Gauss elimination has wrong rank.") - if _compute_rank(stab[:, 0:num_qubits]) != rank: + if compute_rank(stab[:, 0:num_qubits]) != rank: raise QiskitError("The matrix after Gauss elimination has wrong rank.") # validate that we have a num_qubits - rank zero rows for i in range(rank, num_qubits): @@ -236,8 +238,8 @@ def _create_graph_state(cliff, validate=False): # validate that a layer of Hadamard gates and then appending cliff, provides a graph state. if validate: - stabh = cliffh.stab_x - if _compute_rank(stabh) != num_qubits: + stabh = (cliffh.stab_x).astype(bool, copy=False) + if compute_rank(stabh) != num_qubits: raise QiskitError("The state is not a graph state.") return H1_circ, cliffh @@ -267,7 +269,7 @@ def _decompose_graph_state(cliff, validate, cz_synth_func): """ num_qubits = cliff.num_qubits - rank = _compute_rank(cliff.stab_x) + rank = compute_rank(np.asarray(cliff.stab_x, dtype=bool)) cliff_cpy = cliff.copy() if rank < num_qubits: raise QiskitError("The stabilizer state is not a graph state.") @@ -278,7 +280,7 @@ def _decompose_graph_state(cliff, validate, cz_synth_func): stabx = cliff.stab_x stabz = cliff.stab_z stabx_inv = calc_inverse_matrix(stabx, validate) - stabz_update = np.matmul(stabx_inv, stabz) % 2 + stabz_update = binary_matmul(stabx_inv, stabz) # Assert that stabz_update is a symmetric matrix. if validate: @@ -340,7 +342,7 @@ def _decompose_hadamard_free( if not (stabx == np.zeros((num_qubits, num_qubits))).all(): raise QiskitError("The given Clifford is not Hadamard-free.") - destabz_update = np.matmul(calc_inverse_matrix(destabx), destabz) % 2 + destabz_update = binary_matmul(calc_inverse_matrix(destabx), destabz) # Assert that destabz_update is a symmetric matrix. if validate: if (destabz_update != destabz_update.T).any(): diff --git a/qiskit/synthesis/linear/__init__.py b/qiskit/synthesis/linear/__init__.py index 115fc557bfa..f3537de9c3f 100644 --- a/qiskit/synthesis/linear/__init__.py +++ b/qiskit/synthesis/linear/__init__.py @@ -18,6 +18,7 @@ random_invertible_binary_matrix, calc_inverse_matrix, check_invertible_binary_matrix, + binary_matmul, ) # This is re-import is kept for compatibility with Terra 0.23. Eligible for deprecation in 0.25+. diff --git a/qiskit/synthesis/linear/linear_depth_lnn.py b/qiskit/synthesis/linear/linear_depth_lnn.py index 2811b755fa4..7c7360915e0 100644 --- a/qiskit/synthesis/linear/linear_depth_lnn.py +++ b/qiskit/synthesis/linear/linear_depth_lnn.py @@ -28,15 +28,15 @@ from qiskit.synthesis.linear.linear_matrix_utils import ( calc_inverse_matrix, check_invertible_binary_matrix, - _col_op, - _row_op, + col_op, + row_op, ) def _row_op_update_instructions(cx_instructions, mat, a, b): # Add a cx gate to the instructions and update the matrix mat cx_instructions.append((a, b)) - _row_op(mat, a, b) + row_op(mat, a, b) def _get_lower_triangular(n, mat, mat_inv): @@ -62,7 +62,7 @@ def _get_lower_triangular(n, mat, mat_inv): first_j = j else: # cx_instructions_cols (L instructions) are not needed - _col_op(mat, j, first_j) + col_op(mat, j, first_j) # Use row operations directed upwards to zero out all "1"s above the remaining "1" in row i for k in reversed(range(0, i)): if mat[k, first_j]: @@ -70,8 +70,8 @@ def _get_lower_triangular(n, mat, mat_inv): # Apply only U instructions to get the permuted L for inst in cx_instructions_rows: - _row_op(mat_t, inst[0], inst[1]) - _col_op(mat_inv_t, inst[0], inst[1]) + row_op(mat_t, inst[0], inst[1]) + col_op(mat_inv_t, inst[0], inst[1]) return mat_t, mat_inv_t @@ -222,7 +222,7 @@ def _optimize_cx_circ_depth_5n_line(mat): # According to [1] the synthesis is done on the inverse matrix # so the matrix mat is inverted at this step - mat_inv = mat.copy() + mat_inv = mat.astype(bool, copy=True) mat_cpy = calc_inverse_matrix(mat_inv) n = len(mat_cpy) diff --git a/qiskit/synthesis/linear/linear_matrix_utils.py b/qiskit/synthesis/linear/linear_matrix_utils.py index 7a5b6064147..a76efdbb8d7 100644 --- a/qiskit/synthesis/linear/linear_matrix_utils.py +++ b/qiskit/synthesis/linear/linear_matrix_utils.py @@ -12,164 +12,16 @@ """Utility functions for handling binary matrices.""" -from typing import Optional, Union -import numpy as np -from qiskit.exceptions import QiskitError - - -def check_invertible_binary_matrix(mat: np.ndarray): - """Check that a binary matrix is invertible. - - Args: - mat: a binary matrix. - - Returns: - bool: True if mat in invertible and False otherwise. - """ - if len(mat.shape) != 2 or mat.shape[0] != mat.shape[1]: - return False - - rank = _compute_rank(mat) - return rank == mat.shape[0] - - -def random_invertible_binary_matrix( - num_qubits: int, seed: Optional[Union[np.random.Generator, int]] = None -): - """Generates a random invertible n x n binary matrix. - - Args: - num_qubits: the matrix size. - seed: a random seed. - - Returns: - np.ndarray: A random invertible binary matrix of size num_qubits. - """ - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) - - rank = 0 - while rank != num_qubits: - mat = rng.integers(2, size=(num_qubits, num_qubits)) - rank = _compute_rank(mat) - return mat - - -def _gauss_elimination(mat, ncols=None, full_elim=False): - """Gauss elimination of a matrix mat with m rows and n columns. - If full_elim = True, it allows full elimination of mat[:, 0 : ncols] - Returns the matrix mat.""" - - mat, _ = _gauss_elimination_with_perm(mat, ncols, full_elim) - return mat - - -def _gauss_elimination_with_perm(mat, ncols=None, full_elim=False): - """Gauss elimination of a matrix mat with m rows and n columns. - If full_elim = True, it allows full elimination of mat[:, 0 : ncols] - Returns the matrix mat, and the permutation perm that was done on the rows during the process. - perm[0 : rank] represents the indices of linearly independent rows in the original matrix.""" - - # Treat the matrix A as containing integer values - mat = np.array(mat, dtype=int, copy=True) - - m = mat.shape[0] # no. of rows - n = mat.shape[1] # no. of columns - if ncols is not None: - n = min(n, ncols) # no. of active columns - - perm = np.array(range(m)) # permutation on the rows - - r = 0 # current rank - k = 0 # current pivot column - while (r < m) and (k < n): - is_non_zero = False - new_r = r - for j in range(k, n): - for i in range(r, m): - if mat[i][j]: - is_non_zero = True - k = j - new_r = i - break - if is_non_zero: - break - if not is_non_zero: - return mat, perm # A is in the canonical form - - if new_r != r: - mat[[r, new_r]] = mat[[new_r, r]] - perm[r], perm[new_r] = perm[new_r], perm[r] - - if full_elim: - for i in range(0, r): - if mat[i][k]: - mat[i] = mat[i] ^ mat[r] - - for i in range(r + 1, m): - if mat[i][k]: - mat[i] = mat[i] ^ mat[r] - r += 1 - - return mat, perm - - -def calc_inverse_matrix(mat: np.ndarray, verify: bool = False): - """Given a square numpy(dtype=int) matrix mat, tries to compute its inverse. - - Args: - mat: a boolean square matrix. - verify: if True asserts that the multiplication of mat and its inverse is the identity matrix. - - Returns: - np.ndarray: the inverse matrix. - - Raises: - QiskitError: if the matrix is not square. - QiskitError: if the matrix is not invertible. - """ - - if mat.shape[0] != mat.shape[1]: - raise QiskitError("Matrix to invert is a non-square matrix.") - - n = mat.shape[0] - # concatenate the matrix and identity - mat1 = np.concatenate((mat, np.eye(n, dtype=int)), axis=1) - mat1 = _gauss_elimination(mat1, None, full_elim=True) - - r = _compute_rank_after_gauss_elim(mat1[:, 0:n]) - - if r < n: - raise QiskitError("The matrix is not invertible.") - - matinv = mat1[:, n : 2 * n] - - if verify: - mat2 = np.dot(mat, matinv) % 2 - assert np.array_equal(mat2, np.eye(n)) - - return matinv - - -def _compute_rank_after_gauss_elim(mat): - """Given a matrix A after Gaussian elimination, computes its rank - (i.e. simply the number of nonzero rows)""" - return np.sum(mat.any(axis=1)) - - -def _compute_rank(mat): - """Given a matrix A computes its rank""" - mat = _gauss_elimination(mat) - return np.sum(mat.any(axis=1)) - - -def _row_op(mat, ctrl, trgt): - # Perform ROW operation on a matrix mat - mat[trgt] = mat[trgt] ^ mat[ctrl] - - -def _col_op(mat, ctrl, trgt): - # Perform COL operation on a matrix mat - mat[:, ctrl] = mat[:, trgt] ^ mat[:, ctrl] +# pylint: disable=unused-import +from qiskit._accelerate.synthesis.linear import ( + gauss_elimination, + gauss_elimination_with_perm, + compute_rank_after_gauss_elim, + compute_rank, + calc_inverse_matrix, + binary_matmul, + random_invertible_binary_matrix, + check_invertible_binary_matrix, + row_op, + col_op, +) diff --git a/qiskit/synthesis/stabilizer/stabilizer_decompose.py b/qiskit/synthesis/stabilizer/stabilizer_decompose.py index ecdc1b3257e..ef324bc3cad 100644 --- a/qiskit/synthesis/stabilizer/stabilizer_decompose.py +++ b/qiskit/synthesis/stabilizer/stabilizer_decompose.py @@ -143,7 +143,7 @@ def _calc_pauli_diff_stabilizer(cliff, cliff_target): phase.extend(phase_stab) phase = np.array(phase, dtype=int) - A = cliff.symplectic_matrix.astype(int) + A = cliff.symplectic_matrix.astype(bool, copy=False) Ainv = calc_inverse_matrix(A) # By carefully writing how X, Y, Z gates affect each qubit, all we need to compute diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 668d10f32bc..bbc98662105 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -779,7 +779,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) - mat = high_level_object.linear.astype(int) + mat = high_level_object.linear.astype(bool, copy=False) if use_transposed: mat = np.transpose(mat) @@ -831,7 +831,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) - mat = high_level_object.linear.astype(int) + mat = high_level_object.linear.astype(bool, copy=False) if use_transposed: mat = np.transpose(mat) diff --git a/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml b/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml new file mode 100644 index 00000000000..a8e9ec74380 --- /dev/null +++ b/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml @@ -0,0 +1,8 @@ +--- +features_synthesis: + - | + Port internal binary matrix utils from Python to Rust, including + binary matrix multiplication, gaussian elimination, rank calculation, + binary matrix inversion, and random invertible binary matrix generation. + These functions are not part of the Qiskit API, and porting them to rust + improves the performance of certain synthesis methods. diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index a2d868fbc1b..a3df1e9664a 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -86,7 +86,8 @@ def random_linear_circuit( elif name == "linear": nqargs = rng.choice(range(1, num_qubits + 1)) qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - mat = random_invertible_binary_matrix(nqargs, seed=rng) + seed = rng.integers(100000, size=1, dtype=np.uint64)[0] + mat = random_invertible_binary_matrix(nqargs, seed=seed) circ.append(LinearFunction(mat), qargs) elif name == "permutation": nqargs = rng.choice(range(1, num_qubits + 1)) @@ -140,10 +141,11 @@ def test_conversion_to_matrix_and_back(self, num_qubits): and then synthesizing this linear function to a quantum circuit.""" rng = np.random.default_rng(1234) - for _ in range(10): - for num_gates in [0, 5, 5 * num_qubits]: + for num_gates in [0, 5, 5 * num_qubits]: + seeds = rng.integers(100000, size=10, dtype=np.uint64) + for seed in seeds: # create a random linear circuit - linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=rng) + linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=seed) self.assertIsInstance(linear_circuit, QuantumCircuit) # convert it to a linear function @@ -168,10 +170,11 @@ def test_conversion_to_linear_function_and_back(self, num_qubits): """Test correctness of first synthesizing a linear circuit from a linear function, and then converting this linear circuit to a linear function.""" rng = np.random.default_rng(5678) + seeds = rng.integers(100000, size=10, dtype=np.uint64) - for _ in range(10): + for seed in seeds: # create a random invertible binary matrix - binary_matrix = random_invertible_binary_matrix(num_qubits, seed=rng) + binary_matrix = random_invertible_binary_matrix(num_qubits, seed=seed) # create a linear function with this matrix linear_function = LinearFunction(binary_matrix, validate_input=True) diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 043a9eca78b..36d716d2d85 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -424,9 +424,9 @@ def test_from_linear_function(self, num_qubits): """Test initialization from linear function.""" rng = np.random.default_rng(1234) samples = 50 - - for _ in range(samples): - mat = random_invertible_binary_matrix(num_qubits, seed=rng) + seeds = rng.integers(100000, size=samples, dtype=np.uint64) + for seed in seeds: + mat = random_invertible_binary_matrix(num_qubits, seed=seed) lin = LinearFunction(mat) cliff = Clifford(lin) self.assertTrue(Operator(cliff).equiv(Operator(lin))) diff --git a/test/python/synthesis/test_cx_cz_synthesis.py b/test/python/synthesis/test_cx_cz_synthesis.py index 63353dab95d..28a26df181a 100644 --- a/test/python/synthesis/test_cx_cz_synthesis.py +++ b/test/python/synthesis/test_cx_cz_synthesis.py @@ -39,8 +39,9 @@ def test_cx_cz_synth_lnn(self, num_qubits): rng = np.random.default_rng(seed) num_gates = 10 num_trials = 8 + seeds = rng.integers(100000, size=num_trials, dtype=np.uint64) - for _ in range(num_trials): + for seed in seeds: # Generate a random CZ circuit mat_z = np.zeros((num_qubits, num_qubits)) cir_z = QuantumCircuit(num_qubits) @@ -55,7 +56,7 @@ def test_cx_cz_synth_lnn(self, num_qubits): mat_z[j][i] = (mat_z[j][i] + 1) % 2 # Generate a random CX circuit - mat_x = random_invertible_binary_matrix(num_qubits, seed=rng) + mat_x = random_invertible_binary_matrix(num_qubits, seed=seed) mat_x = np.array(mat_x, dtype=bool) cir_x = synth_cnot_depth_line_kms(mat_x) diff --git a/test/python/synthesis/test_linear_synthesis.py b/test/python/synthesis/test_linear_synthesis.py index 98a49b6642f..bbfab20a30f 100644 --- a/test/python/synthesis/test_linear_synthesis.py +++ b/test/python/synthesis/test_linear_synthesis.py @@ -24,6 +24,7 @@ random_invertible_binary_matrix, check_invertible_binary_matrix, calc_inverse_matrix, + binary_matmul, ) from qiskit.synthesis.linear.linear_circuits_utils import transpose_cx_circ, optimize_cx_4_options from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -107,8 +108,9 @@ def test_invertible_matrix(self, n): """Test the functions for generating a random invertible matrix and inverting it.""" mat = random_invertible_binary_matrix(n, seed=1234) out = check_invertible_binary_matrix(mat) + mat = mat.astype(bool) mat_inv = calc_inverse_matrix(mat, verify=True) - mat_out = np.dot(mat, mat_inv) % 2 + mat_out = binary_matmul(mat, mat_inv) self.assertTrue(np.array_equal(mat_out, np.eye(n))) self.assertTrue(out) @@ -117,8 +119,9 @@ def test_synth_lnn_kms(self, num_qubits): """Test that synth_cnot_depth_line_kms produces the correct synthesis.""" rng = np.random.default_rng(1234) num_trials = 10 - for _ in range(num_trials): - mat = random_invertible_binary_matrix(num_qubits, seed=rng) + seeds = rng.integers(100000, size=num_trials, dtype=np.uint64) + for seed in seeds: + mat = random_invertible_binary_matrix(num_qubits, seed=seed) mat = np.array(mat, dtype=bool) qc = synth_cnot_depth_line_kms(mat) mat1 = LinearFunction(qc).linear diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index ff54169374b..a76ab08d90e 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -534,22 +534,22 @@ def test_section_size(self): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 1})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 22) - self.assertEqual(qct.depth(), 20) + self.assertEqual(qct.size(), 30) + self.assertEqual(qct.depth(), 27) with self.subTest("section_size_2"): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 2})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 23) - self.assertEqual(qct.depth(), 19) + self.assertEqual(qct.size(), 27) + self.assertEqual(qct.depth(), 23) with self.subTest("section_size_3"): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 3})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 23) - self.assertEqual(qct.depth(), 17) + self.assertEqual(qct.size(), 29) + self.assertEqual(qct.depth(), 23) def test_invert_and_transpose(self): """Test that the plugin takes the use_inverted and use_transposed arguments into account.""" @@ -623,7 +623,7 @@ def test_plugin_selection_all_with_metrix(self): # The seed is chosen so that we get different best circuits depending on whether we # want to minimize size or depth. - mat = random_invertible_binary_matrix(7, seed=37) + mat = random_invertible_binary_matrix(7, seed=38) qc = QuantumCircuit(7) qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) @@ -641,8 +641,8 @@ def test_plugin_selection_all_with_metrix(self): ) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 20) - self.assertEqual(qct.depth(), 15) + self.assertEqual(qct.size(), 23) + self.assertEqual(qct.depth(), 19) with self.subTest("depth_fn"): # We want to minimize the "depth" (aka the number of layers) in the circuit @@ -658,8 +658,8 @@ def test_plugin_selection_all_with_metrix(self): ) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 23) - self.assertEqual(qct.depth(), 12) + self.assertEqual(qct.size(), 24) + self.assertEqual(qct.depth(), 13) class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): From 70561982429ee45640b87773a697c3d98d4247c6 Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:02:51 +0300 Subject: [PATCH 155/159] Add Rust representation for RXX, RYY, RZZ & RZX gates (#12672) * Updating tables * Updating mapping tables * Adding remaining functionality * Handling Black, clippy, fmt --- crates/circuit/src/gate_matrix.rs | 60 ++++++++++ crates/circuit/src/imports.rs | 8 +- crates/circuit/src/operations.rs | 114 +++++++++++++++++-- qiskit/circuit/library/standard_gates/rxx.py | 3 + qiskit/circuit/library/standard_gates/ryy.py | 3 + qiskit/circuit/library/standard_gates/rzx.py | 3 + qiskit/circuit/library/standard_gates/rzz.py | 3 + qiskit/circuit/quantumcircuit.py | 16 +-- 8 files changed, 185 insertions(+), 25 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 074b1c2ac68..d5965afc496 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -309,3 +309,63 @@ pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { [C_ZERO, C_ZERO, C_ZERO, C_ONE], ] } + +#[inline] +pub fn rxx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csinm = c64(0., -sint); + let c0 = c64(0., 0.); + + [ + [ccos, c0, c0, csinm], + [c0, ccos, csinm, c0], + [c0, csinm, ccos, c0], + [csinm, c0, c0, ccos], + ] +} + +#[inline] +pub fn ryy_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + let c0 = c64(0., 0.); + + [ + [ccos, c0, c0, csin], + [c0, ccos, -csin, c0], + [c0, -csin, ccos, c0], + [csin, c0, c0, ccos], + ] +} + +#[inline] +pub fn rzz_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let c0 = c64(0., 0.); + let exp_it2 = c64(cost, sint); + let exp_mit2 = c64(cost, -sint); + + [ + [exp_mit2, c0, c0, c0], + [c0, exp_it2, c0, c0], + [c0, c0, exp_it2, c0], + [c0, c0, c0, exp_mit2], + ] +} + +#[inline] +pub fn rzx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + let c0 = c64(0., 0.); + + [ + [ccos, c0, -csin, c0], + [c0, ccos, c0, csin], + [-csin, c0, ccos, c0], + [c0, csin, c0, ccos], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 530e635c94f..53fee34f486 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -191,13 +191,13 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // RC3XGate = 48 ["placeholder", "placeholder"], // RXXGate = 49 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rxx", "RXXGate"], // RYYGate = 50 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.ryy", "RYYGate"], // RZZGate = 51 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rzz", "RZZGate"], // RZXGate = 52 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rzx", "RZXGate"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 85192b63dbd..3ac17e4da8f 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -237,8 +237,8 @@ static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, // 20-29 2, 2, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 - 2, 2, 34, 34, 34, 2, 34, 34, 34, 34, // 40-49 - 34, 34, 34, // 50-52 + 2, 2, 34, 34, 34, 2, 34, 34, 34, 2, // 40-49 + 2, 2, 2, // 50-52 ]; // TODO: replace all 34s (placeholders) with actual number @@ -247,8 +247,8 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 0, 0, 0, 0, 2, 2, 1, 2, 3, 1, // 20-29 1, 1, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 - 1, 3, 34, 34, 34, 0, 34, 34, 34, 34, // 40-49 - 34, 34, 34, // 50-52 + 1, 3, 34, 34, 34, 0, 34, 34, 34, 1, // 40-49 + 1, 1, 1, // 50-52 ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -542,8 +542,22 @@ impl Operation for StandardGate { }, Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), - Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), - Self::RZXGate => todo!(), + Self::RXXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rxx_gate(theta)).to_owned()), + _ => None, + }, + Self::RYYGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::ryy_gate(theta)).to_owned()), + _ => None, + }, + Self::RZZGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzz_gate(theta)).to_owned()), + _ => None, + }, + Self::RZXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzx_gate(theta)).to_owned()), + _ => None, + }, } } @@ -1103,8 +1117,90 @@ impl Operation for StandardGate { Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), - Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), - Self::RZXGate => todo!(), + Self::RXXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q0.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + (Self::HGate, smallvec![], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q0.clone()), + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q0), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1), + (Self::CXGate, smallvec![], q0_q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), } } @@ -1115,7 +1211,7 @@ impl Operation for StandardGate { const FLOAT_ZERO: Param = Param::Float(0.0); -// Return explictly requested copy of `param`, handling +// Return explicitly requested copy of `param`, handling // each variant separately. fn clone_param(param: &Param, py: Python) -> Param { match param { diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index c4e35e53d55..1c06ae05a85 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXXGate(Gate): @@ -72,6 +73,8 @@ class RXXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 98847b7b218..91d7d8096cf 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYYGate(Gate): @@ -72,6 +73,8 @@ class RYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 1f930ab422d..90e7b71c0a3 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZXGate(Gate): @@ -117,6 +118,8 @@ class RZXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 5ca974764d3..119dd370e20 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZZGate(Gate): @@ -84,6 +85,8 @@ class RZZGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZZGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7b8fe6e031f..ef160ec064c 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4802,9 +4802,7 @@ def rxx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rxx import RXXGate - - return self.append(RXXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RXXGate, [theta], [qubit1, qubit2]) def ry( self, theta: ParameterValueType, qubit: QubitSpecifier, label: str | None = None @@ -4877,9 +4875,7 @@ def ryy( Returns: A handle to the instructions created. """ - from .library.standard_gates.ryy import RYYGate - - return self.append(RYYGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RYYGate, [theta], [qubit1, qubit2]) def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.RZGate`. @@ -4949,9 +4945,7 @@ def rzx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzx import RZXGate - - return self.append(RZXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZXGate, [theta], [qubit1, qubit2]) def rzz( self, theta: ParameterValueType, qubit1: QubitSpecifier, qubit2: QubitSpecifier @@ -4968,9 +4962,7 @@ def rzz( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzz import RZZGate - - return self.append(RZZGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZZGate, [theta], [qubit1, qubit2]) def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.ECRGate`. From c452694d70bfa17df8419335c7fb138c9b81963b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:27:45 +0200 Subject: [PATCH 156/159] Bump rustworkx-core from 0.14.2 to 0.15.0 (#12682) Bumps [rustworkx-core](https://github.com/Qiskit/rustworkx) from 0.14.2 to 0.15.0. - [Release notes](https://github.com/Qiskit/rustworkx/releases) - [Commits](https://github.com/Qiskit/rustworkx/compare/0.14.2...0.15.0) --- updated-dependencies: - dependency-name: rustworkx-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthew Treinish --- Cargo.lock | 38 ++++++++++++++---------------------- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68a3d321406..380db7394bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,16 +548,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -906,7 +896,7 @@ checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" dependencies = [ "cov-mark", "either", - "indexmap 2.2.6", + "indexmap", "itertools 0.10.5", "once_cell", "oq3_lexer", @@ -996,12 +986,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -1018,12 +1008,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "priority-queue" -version = "1.4.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", - "indexmap 1.9.3", + "equivalent", + "indexmap", ] [[package]] @@ -1104,7 +1095,7 @@ checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "indoc", "libc", "memoffset", @@ -1173,7 +1164,7 @@ dependencies = [ "faer", "faer-ext", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "itertools 0.13.0", "ndarray", "num-bigint", @@ -1228,7 +1219,7 @@ name = "qiskit-qasm3" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "oq3_semantics", "pyo3", ] @@ -1399,14 +1390,15 @@ checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rustworkx-core" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +checksum = "c2b9aa5926b35dd3029530aef27eac0926b544c78f8e8f1aad4d37854b132fe9" dependencies = [ "ahash 0.8.11", "fixedbitset", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", + "ndarray", "num-traits", "petgraph", "priority-queue", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index b377a9b38a6..87524309651 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -19,7 +19,7 @@ ahash = "0.8.11" num-traits = "0.2" num-complex.workspace = true num-bigint = "0.4" -rustworkx-core = "0.14" +rustworkx-core = "0.15" faer = "0.19.1" itertools = "0.13.0" qiskit-circuit.workspace = true From a7fc2daf4c0fb0d7b10511d40bde8d1b3201ebce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:49:06 +0200 Subject: [PATCH 157/159] Add Rust representation for CHGate, CPhaseGate, CSGate, CSdgGate, CSXGate, CSwapGate (#12639) * Add CHGate, CPhaseGate, CSGate, CSdgGate, CSXGate, CSwapGate * Fix tests, add labels * Fix oversights in gate definitions * Fix test * Add ctrl_state 1 to rust building path. --- crates/circuit/src/gate_matrix.rs | 185 ++++++++++----- crates/circuit/src/operations.rs | 210 +++++++++++++++--- qiskit/circuit/library/standard_gates/h.py | 2 + qiskit/circuit/library/standard_gates/p.py | 2 + qiskit/circuit/library/standard_gates/s.py | 4 + qiskit/circuit/library/standard_gates/swap.py | 2 + qiskit/circuit/library/standard_gates/sx.py | 2 + qiskit/circuit/quantumcircuit.py | 162 +++++++++----- test/python/circuit/test_rust_equivalence.py | 14 +- 9 files changed, 436 insertions(+), 147 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index d5965afc496..46585ff6da6 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -53,43 +53,6 @@ pub fn rz_gate(theta: f64) -> GateArray1Q { [[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } -#[inline] -pub fn crx_gate(theta: f64) -> GateArray2Q { - let half_theta = theta / 2.; - let cos = c64(half_theta.cos(), 0.); - let isin = c64(0., half_theta.sin()); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, cos, C_ZERO, -isin], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, -isin, C_ZERO, cos], - ] -} - -#[inline] -pub fn cry_gate(theta: f64) -> GateArray2Q { - let half_theta = theta / 2.; - let cos = c64(half_theta.cos(), 0.); - let sin = c64(half_theta.sin(), 0.); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, cos, C_ZERO, -sin], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, sin, C_ZERO, cos], - ] -} - -#[inline] -pub fn crz_gate(theta: f64) -> GateArray2Q { - let i_half_theta = c64(0., theta / 2.); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], - ] -} - pub static H_GATE: GateArray1Q = [ [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], @@ -210,6 +173,71 @@ pub static TDG_GATE: GateArray1Q = [ [C_ZERO, c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], ]; +pub static CH_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + ], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(-FRAC_1_SQRT_2, 0.), + ], +]; + +pub static CS_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, IM], +]; + +pub static CSDG_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, M_IM], +]; + +pub static CSX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, c64(0.5, 0.5), C_ZERO, c64(0.5, -0.5)], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, c64(0.5, -0.5), C_ZERO, c64(0.5, 0.5)], +]; + +pub static CSWAP_GATE: GateArray3Q = [ + [ + C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, + ], +]; + pub static DCX_GATE: GateArray2Q = [ [C_ONE, C_ZERO, C_ZERO, C_ZERO], [C_ZERO, C_ZERO, C_ZERO, C_ONE], @@ -217,6 +245,43 @@ pub static DCX_GATE: GateArray2Q = [ [C_ZERO, C_ZERO, C_ONE, C_ZERO], ]; +#[inline] +pub fn crx_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., half_theta.sin()); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -isin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, -isin, C_ZERO, cos], + ] +} + +#[inline] +pub fn cry_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -sin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, sin, C_ZERO, cos], + ] +} + +#[inline] +pub fn crz_gate(theta: f64) -> GateArray2Q { + let i_half_theta = c64(0., theta / 2.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], + ] +} + #[inline] pub fn global_phase_gate(theta: f64) -> GateArray0Q { [[c64(0., theta).exp()]] @@ -310,18 +375,27 @@ pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { ] } +#[inline] +pub fn cp_gate(lam: f64) -> GateArray2Q { + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, c64(0., lam).exp()], + ] +} + #[inline] pub fn rxx_gate(theta: f64) -> GateArray2Q { let (sint, cost) = (theta / 2.0).sin_cos(); let ccos = c64(cost, 0.); let csinm = c64(0., -sint); - let c0 = c64(0., 0.); [ - [ccos, c0, c0, csinm], - [c0, ccos, csinm, c0], - [c0, csinm, ccos, c0], - [csinm, c0, c0, ccos], + [ccos, C_ZERO, C_ZERO, csinm], + [C_ZERO, ccos, csinm, C_ZERO], + [C_ZERO, csinm, ccos, C_ZERO], + [csinm, C_ZERO, C_ZERO, ccos], ] } @@ -330,28 +404,26 @@ pub fn ryy_gate(theta: f64) -> GateArray2Q { let (sint, cost) = (theta / 2.0).sin_cos(); let ccos = c64(cost, 0.); let csin = c64(0., sint); - let c0 = c64(0., 0.); [ - [ccos, c0, c0, csin], - [c0, ccos, -csin, c0], - [c0, -csin, ccos, c0], - [csin, c0, c0, ccos], + [ccos, C_ZERO, C_ZERO, csin], + [C_ZERO, ccos, -csin, C_ZERO], + [C_ZERO, -csin, ccos, C_ZERO], + [csin, C_ZERO, C_ZERO, ccos], ] } #[inline] pub fn rzz_gate(theta: f64) -> GateArray2Q { let (sint, cost) = (theta / 2.0).sin_cos(); - let c0 = c64(0., 0.); let exp_it2 = c64(cost, sint); let exp_mit2 = c64(cost, -sint); [ - [exp_mit2, c0, c0, c0], - [c0, exp_it2, c0, c0], - [c0, c0, exp_it2, c0], - [c0, c0, c0, exp_mit2], + [exp_mit2, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, exp_it2, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, exp_it2, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, exp_mit2], ] } @@ -360,12 +432,11 @@ pub fn rzx_gate(theta: f64) -> GateArray2Q { let (sint, cost) = (theta / 2.0).sin_cos(); let ccos = c64(cost, 0.); let csin = c64(0., sint); - let c0 = c64(0., 0.); [ - [ccos, c0, -csin, c0], - [c0, ccos, c0, csin], - [-csin, c0, ccos, c0], - [c0, csin, c0, ccos], + [ccos, C_ZERO, -csin, C_ZERO], + [C_ZERO, ccos, C_ZERO, csin], + [-csin, C_ZERO, ccos, C_ZERO], + [C_ZERO, csin, C_ZERO, ccos], ] } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 3ac17e4da8f..df15b4abb41 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -522,20 +522,38 @@ impl Operation for StandardGate { } _ => None, }, + Self::CHGate => match params { + [] => Some(aview2(&gate_matrix::CH_GATE).to_owned()), + _ => None, + }, + Self::CPhaseGate => match params { + [Param::Float(lam)] => Some(aview2(&gate_matrix::cp_gate(*lam)).to_owned()), + _ => None, + }, + Self::CSGate => match params { + [] => Some(aview2(&gate_matrix::CS_GATE).to_owned()), + _ => None, + }, + Self::CSdgGate => match params { + [] => Some(aview2(&gate_matrix::CSDG_GATE).to_owned()), + _ => None, + }, + Self::CSXGate => match params { + [] => Some(aview2(&gate_matrix::CSX_GATE).to_owned()), + _ => None, + }, + Self::CSwapGate => match params { + [] => Some(aview2(&gate_matrix::CSWAP_GATE).to_owned()), + _ => None, + }, + Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), Self::RGate => match params { [Param::Float(theta), Param::Float(phi)] => { Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) } _ => None, }, - Self::CHGate => todo!(), - Self::CPhaseGate => todo!(), - Self::CSGate => todo!(), - Self::CSdgGate => todo!(), - Self::CSXGate => todo!(), - Self::CSwapGate => todo!(), - Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), - Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), Self::DCXGate => match params { [] => Some(aview2(&gate_matrix::DCX_GATE).to_owned()), _ => None, @@ -870,14 +888,14 @@ impl Operation for StandardGate { ) }), Self::UGate => None, - Self::SGate => Python::with_gil(|py| -> Option { + Self::U1Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( Self::PhaseGate, - smallvec![Param::Float(PI / 2.)], + params.iter().cloned().collect(), smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -885,14 +903,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U1Gate => Python::with_gil(|py| -> Option { + Self::U2Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::PhaseGate, - params.iter().cloned().collect(), + Self::UGate, + smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -900,14 +918,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::SdgGate => Python::with_gil(|py| -> Option { + Self::U3Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::PhaseGate, - smallvec![Param::Float(-PI / 2.)], + Self::UGate, + params.iter().cloned().collect(), smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -915,14 +933,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U2Gate => Python::with_gil(|py| -> Option { + Self::SGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::UGate, - smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], + Self::PhaseGate, + smallvec![Param::Float(PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -930,14 +948,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::TGate => Python::with_gil(|py| -> Option { + Self::SdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( Self::PhaseGate, - smallvec![Param::Float(PI / 4.)], + smallvec![Param::Float(-PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -945,14 +963,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U3Gate => Python::with_gil(|py| -> Option { + Self::TGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::UGate, - params.iter().cloned().collect(), + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -1075,6 +1093,143 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::CHGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::TGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::TdgGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::SdgGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CPhaseGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q0, + ), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q1, + ), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(-PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSdgGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CPhaseGate, smallvec![Param::Float(PI / 2.)], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 3, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ( + Self::CCXGate, + smallvec![], + smallvec![Qubit(0), Qubit(1), Qubit(2)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::RGate => Python::with_gil(|py| -> Option { let theta_expr = clone_param(¶ms[0], py); let phi_expr1 = add_param(¶ms[1], -PI / 2., py); @@ -1090,12 +1245,6 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CHGate => todo!(), - Self::CPhaseGate => todo!(), - Self::CSGate => todo!(), - Self::CSdgGate => todo!(), - Self::CSXGate => todo!(), - Self::CSwapGate => todo!(), Self::CUGate => todo!(), Self::CU1Gate => todo!(), Self::CU3Gate => todo!(), @@ -1114,7 +1263,6 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), Self::RXXGate => Python::with_gil(|py| -> Option { diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 2d273eed74d..c07895ebbea 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -185,6 +185,8 @@ class CHGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CHGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 1a792649fea..8c83aa46402 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -200,6 +200,8 @@ class CPhaseGate(ControlledGate): phase difference. """ + _standard_gate = StandardGate.CPhaseGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index f62d16a10d4..975d1cb3be8 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -215,6 +215,8 @@ class CSGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSGate + def __init__( self, label: Optional[str] = None, @@ -301,6 +303,8 @@ class CSdgGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSdgGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 243a84701ef..5d33bc74b8d 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -216,6 +216,8 @@ class CSwapGate(SingletonControlledGate): |1, b, c\rangle \rightarrow |1, c, b\rangle """ + _standard_gate = StandardGate.CSwapGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 72e4a8f9b5b..ec3c8765314 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -266,6 +266,8 @@ class CSXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CSXGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ef160ec064c..0019a15443e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4516,6 +4516,12 @@ def ch( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CHGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.h import CHGate return self.append( @@ -4593,6 +4599,12 @@ def cp( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CPhaseGate, [theta], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.p import CPhaseGate return self.append( @@ -4772,14 +4784,14 @@ def crx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rx import CRXGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRXGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.rx import CRXGate + return self.append( CRXGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4845,14 +4857,14 @@ def cry( Returns: A handle to the instructions created. """ - from .library.standard_gates.ry import CRYGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRYGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.ry import CRYGate + return self.append( CRYGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4915,14 +4927,14 @@ def crz( Returns: A handle to the instructions created. """ - from .library.standard_gates.rz import CRZGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRZGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.rz import CRZGate + return self.append( CRZGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4975,9 +4987,7 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate( - StandardGate.ECRGate, [], qargs=[qubit1, qubit2], cargs=None - ) + return self._append_standard_gate(StandardGate.ECRGate, [], qargs=[qubit1, qubit2]) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -4990,7 +5000,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.SGate, [], qargs=[qubit]) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -5003,7 +5013,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SdgGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.SdgGate, [], qargs=[qubit]) def cs( self, @@ -5027,6 +5037,12 @@ def cs( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSGate return self.append( @@ -5058,6 +5074,12 @@ def csdg( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSdgGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSdgGate return self.append( @@ -5082,7 +5104,6 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet StandardGate.SwapGate, [], qargs=[qubit1, qubit2], - cargs=None, ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5096,7 +5117,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ISwapGate, [], [qubit1, qubit2], cargs=None) + return self._append_standard_gate(StandardGate.ISwapGate, [], qargs=[qubit1, qubit2]) def cswap( self, @@ -5122,6 +5143,15 @@ def cswap( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSwapGate, + [], + qargs=[control_qubit, target_qubit1, target_qubit2], + label=label, + ) + from .library.standard_gates.swap import CSwapGate return self.append( @@ -5142,7 +5172,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXGate, [], qargs=[qubit]) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -5155,7 +5185,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXdgGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXdgGate, [], qargs=[qubit]) def csx( self, @@ -5179,6 +5209,12 @@ def csx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSXGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.sx import CSXGate return self.append( @@ -5199,7 +5235,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.TGate, [], qargs=[qubit]) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -5212,7 +5248,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TdgGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.TdgGate, [], qargs=[qubit]) def u( self, @@ -5311,17 +5347,23 @@ def cx( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.x import CXGate - - return self.append( - CXGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CXGate, [], - copy=False, + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, ) - return self._append_standard_gate( - StandardGate.CXGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + + from .library.standard_gates.x import CXGate + + return self.append( + CXGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5360,20 +5402,22 @@ def ccx( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.x import CCXGate - - return self.append( - CCXGate(ctrl_state=ctrl_state), - [control_qubit1, control_qubit2, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CCXGate, [], - copy=False, + qargs=[control_qubit1, control_qubit2, target_qubit], + cargs=None, ) - return self._append_standard_gate( - StandardGate.CCXGate, + + from .library.standard_gates.x import CCXGate + + return self.append( + CCXGate(ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], [], - qargs=[control_qubit1, control_qubit2, target_qubit], - cargs=None, + copy=False, ) def mcx( @@ -5495,18 +5539,23 @@ def cy( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.y import CYGate - - return self.append( - CYGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CYGate, [], - copy=False, + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, ) - return self._append_standard_gate( - StandardGate.CYGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + from .library.standard_gates.y import CYGate + + return self.append( + CYGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def z(self, qubit: QubitSpecifier) -> InstructionSet: @@ -5544,18 +5593,19 @@ def cz( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.z import CZGate - - return self.append( - CZGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], label=label ) - return self._append_standard_gate( - StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + from .library.standard_gates.z import CZGate + + return self.append( + CZGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def ccz( diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index b20db4c79f9..6c0cc977e58 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -73,6 +73,7 @@ def test_definitions(self): self.assertIsNone(rs_def) else: rs_def = QuantumCircuit._from_circuit_data(rs_def) + for rs_inst, py_inst in zip(rs_def._data, py_def._data): # Rust uses U but python still uses U3 and u2 if rs_inst.operation.name == "u": @@ -92,8 +93,8 @@ def test_definitions(self): [py_def.find_bit(x).index for x in py_inst.qubits], [rs_def.find_bit(x).index for x in rs_inst.qubits], ) - # Rust uses P but python still uses u1 - elif rs_inst.operation.name == "p": + # Rust uses p but python still uses u1/u3 in some cases + elif rs_inst.operation.name == "p" and not name in ["cp", "cs", "csdg"]: if py_inst.operation.name == "u1": self.assertEqual(py_inst.operation.name, "u1") self.assertEqual(rs_inst.operation.params, py_inst.operation.params) @@ -110,7 +111,14 @@ def test_definitions(self): [py_def.find_bit(x).index for x in py_inst.qubits], [rs_def.find_bit(x).index for x in rs_inst.qubits], ) - + # Rust uses cp but python still uses cu1 in some cases + elif rs_inst.operation.name == "cp": + self.assertEqual(py_inst.operation.name, "cu1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) else: self.assertEqual(py_inst.operation.name, rs_inst.operation.name) self.assertEqual(rs_inst.operation.params, py_inst.operation.params) From 373e8a68c852a445fa9b11548a437c09e34d2d74 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 1 Jul 2024 13:59:21 +0100 Subject: [PATCH 158/159] Encapsulate Python sequence-like indexers (#12669) This encapsulates a lot of the common logic around Python sequence-like indexers (`SliceOrInt`) into iterators that handle adapting negative indices and slices in `usize` for containers of a given size. These indexers now all implement `ExactSizeIterator` and `DoubleEndedIterator`, so they can be used with all `Iterator` methods, and can be used (with `Iterator::map` and friends) as inputs to `PyList::new_bound`, which makes code simpler at all points of use. The special-cased uses of this kind of thing from `CircuitData` are replaced with the new forms. This had no measurable impact on performance on my machine, and removes a lot noise from error-handling and highly specialised functions. --- Cargo.lock | 2 + Cargo.toml | 1 + crates/accelerate/Cargo.toml | 1 + .../src/euler_one_qubit_decomposer.rs | 55 +-- crates/accelerate/src/two_qubit_decompose.rs | 59 +-- crates/circuit/Cargo.toml | 1 + crates/circuit/src/circuit_data.rs | 307 +++++--------- crates/circuit/src/lib.rs | 12 +- crates/circuit/src/slice.rs | 375 ++++++++++++++++++ 9 files changed, 517 insertions(+), 296 deletions(-) create mode 100644 crates/circuit/src/slice.rs diff --git a/Cargo.lock b/Cargo.lock index 380db7394bc..6ce26b3baea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,6 +1180,7 @@ dependencies = [ "rayon", "rustworkx-core", "smallvec", + "thiserror", ] [[package]] @@ -1192,6 +1193,7 @@ dependencies = [ "numpy", "pyo3", "smallvec", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 13f43cfabcd..a6ccf60f7f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" +thiserror = "1.0" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 87524309651..9d602478399 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -23,6 +23,7 @@ rustworkx-core = "0.15" faer = "0.19.1" itertools = "0.13.0" qiskit-circuit.workspace = true +thiserror.workspace = true [dependencies.smallvec] workspace = true diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 9f10f76de46..01725269bb8 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -21,9 +21,9 @@ use std::f64::consts::PI; use std::ops::Deref; use std::str::FromStr; -use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::PyString; +use pyo3::types::{PyList, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; @@ -31,8 +31,8 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::c64; -use qiskit_circuit::SliceOrInt; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -97,46 +97,15 @@ impl OneQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: Vec<(String, SmallVec<[f64; 3]>)> = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=-1 - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 8637cb03c73..37061d5159f 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -21,10 +21,6 @@ use approx::{abs_diff_eq, relative_eq}; use num_complex::{Complex, Complex64, ComplexFloat}; use num_traits::Zero; -use pyo3::exceptions::{PyIndexError, PyValueError}; -use pyo3::prelude::*; -use pyo3::wrap_pyfunction; -use pyo3::Python; use smallvec::{smallvec, SmallVec}; use std::f64::consts::{FRAC_1_SQRT_2, PI}; use std::ops::Deref; @@ -37,7 +33,11 @@ use ndarray::prelude::*; use ndarray::Zip; use numpy::PyReadonlyArray2; use numpy::{IntoPyArray, ToPyArray}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; +use pyo3::types::PyList; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ @@ -52,8 +52,8 @@ use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; -use qiskit_circuit::SliceOrInt; const PI2: f64 = PI / 2.; const PI4: f64 = PI / 4.; @@ -1131,46 +1131,15 @@ impl TwoQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: TwoQubitSequenceVec = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=- - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index dd7e878537d..50160c7bac1 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -14,6 +14,7 @@ hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true +thiserror.workspace = true [dependencies.pyo3] workspace = true diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 07f4579a4cd..10e0691021a 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -22,11 +22,12 @@ use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationType, Param, StandardGate}; use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; -use crate::{Clbit, Qubit, SliceOrInt}; +use crate::slice::{PySequenceIndex, SequenceIndex}; +use crate::{Clbit, Qubit}; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; +use pyo3::types::{PyList, PySet, PyTuple, PyType}; use pyo3::{intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; @@ -321,7 +322,7 @@ impl CircuitData { } pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { - let packed = self.pack(py, value)?; + let packed = self.pack(value)?; let new_index = self.data.len(); self.data.push(packed); self.update_param_table(py, new_index, None) @@ -744,184 +745,130 @@ impl CircuitData { } // Note: we also rely on this to make us iterable! - pub fn __getitem__(&self, py: Python, index: &Bound) -> PyResult { - // Internal helper function to get a specific - // instruction by index. - fn get_at( - self_: &CircuitData, - py: Python<'_>, - index: isize, - ) -> PyResult> { - let index = self_.convert_py_index(index)?; - if let Some(inst) = self_.data.get(index) { - let qubits = self_.qargs_interner.intern(inst.qubits_id); - let clbits = self_.cargs_interner.intern(inst.clbits_id); - Py::new( - py, - CircuitInstruction::new( - py, - inst.op.clone(), - self_.qubits.map_indices(qubits.value), - self_.clbits.map_indices(clbits.value), - inst.params.clone(), - inst.extra_attrs.clone(), - ), - ) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - - if index.is_exact_instance_of::() { - let slice = self.convert_py_slice(index.downcast_exact::()?)?; - let result = slice - .into_iter() - .map(|i| get_at(self, py, i)) - .collect::>>()?; - Ok(result.into_py(py)) - } else { - Ok(get_at(self, py, index.extract()?)?.into_py(py)) + pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult { + // Get a single item, assuming the index is validated as in bounds. + let get_single = |index: usize| { + let inst = &self.data[index]; + let qubits = self.qargs_interner.intern(inst.qubits_id); + let clbits = self.cargs_interner.intern(inst.clbits_id); + CircuitInstruction::new( + py, + inst.op.clone(), + self.qubits.map_indices(qubits.value), + self.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), + ) + .into_py(py) + }; + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => Ok(get_single(index)), + indices => Ok(PyList::new_bound(py, indices.iter().map(get_single)).into_py(py)), } } - pub fn __delitem__(&mut self, py: Python, index: SliceOrInt) -> PyResult<()> { - match index { - SliceOrInt::Slice(slice) => { - let slice = { - let mut s = self.convert_py_slice(&slice)?; - if s.len() > 1 && s.first().unwrap() < s.last().unwrap() { - // Reverse the order so we're sure to delete items - // at the back first (avoids messing up indices). - s.reverse() - } - s - }; - for i in slice.into_iter() { - self.__delitem__(py, SliceOrInt::Int(i))?; - } - self.reindex_parameter_table(py)?; - Ok(()) - } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - if self.data.get(index).is_some() { - if index == self.data.len() { - // For individual removal from param table before - // deletion - self.remove_from_parameter_table(py, index)?; - self.data.remove(index); - } else { - // For delete in the middle delete before reindexing - self.data.remove(index); - self.reindex_parameter_table(py)?; - } - Ok(()) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - } + pub fn __delitem__(&mut self, py: Python, index: PySequenceIndex) -> PyResult<()> { + self.delitem(py, index.with_len(self.data.len())?) } pub fn setitem_no_param_table_update( &mut self, - py: Python<'_>, - index: isize, - value: &Bound, + index: usize, + value: PyRef, ) -> PyResult<()> { - let index = self.convert_py_index(index)?; - let value: PyRef = value.downcast()?.borrow(); - let mut packed = self.pack(py, value)?; + let mut packed = self.pack(value)?; std::mem::swap(&mut packed, &mut self.data[index]); Ok(()) } - pub fn __setitem__( - &mut self, - py: Python<'_>, - index: SliceOrInt, - value: &Bound, - ) -> PyResult<()> { - match index { - SliceOrInt::Slice(slice) => { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - let slice = self.convert_py_slice(&slice)?; - let values = value.iter()?.collect::>>>()?; - if indices.step != 1 && slice.len() != values.len() { - // A replacement of a different length when step isn't exactly '1' - // would result in holes. - return Err(PyValueError::new_err(format!( - "attempt to assign sequence of size {:?} to extended slice of size {:?}", - values.len(), - slice.len(), - ))); - } + 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(); + let mut packed = slf.pack(value.downcast::()?.borrow())?; + slf.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut slf.data[index]); + slf.update_param_table(py, index, None)?; + Ok(()) + } - for (i, v) in slice.iter().zip(values.iter()) { - self.__setitem__(py, SliceOrInt::Int(*i), v)?; + let py = value.py(); + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => set_single(self, index, value), + indices @ SequenceIndex::PosRange { + start, + stop, + step: 1, + } => { + // `list` allows setting a slice with step +1 to an arbitrary length. + let values = value.iter()?.collect::>>()?; + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; } - - if slice.len() > values.len() { - // Delete any extras. - let slice = PySlice::new_bound( + if indices.len() > values.len() { + self.delitem( py, - indices.start + values.len() as isize, - indices.stop, - 1isize, - ); - self.__delitem__(py, SliceOrInt::Slice(slice))?; + SequenceIndex::PosRange { + start: start + values.len(), + stop, + step: 1, + }, + )? } else { - // Insert any extra values. - for v in values.iter().skip(slice.len()).rev() { - let v: PyRef = v.extract()?; - self.insert(py, indices.stop, v)?; + for value in values[indices.len()..].iter().rev() { + self.insert(stop as isize, value.downcast()?.borrow())?; } } - Ok(()) } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - let value: PyRef = value.extract()?; - let mut packed = self.pack(py, value)?; - self.remove_from_parameter_table(py, index)?; - std::mem::swap(&mut packed, &mut self.data[index]); - self.update_param_table(py, index, None)?; - Ok(()) + indices => { + let values = value.iter()?.collect::>>()?; + if indices.len() == values.len() { + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; + } + Ok(()) + } else { + Err(PyValueError::new_err(format!( + "attempt to assign sequence of size {:?} to extended slice of size {:?}", + values.len(), + indices.len(), + ))) + } } } } - pub fn insert( - &mut self, - py: Python<'_>, - index: isize, - value: PyRef, - ) -> PyResult<()> { - let index = self.convert_py_index_clamped(index); - let old_len = self.data.len(); - let packed = self.pack(py, value)?; + pub fn insert(&mut self, mut index: isize, value: PyRef) -> PyResult<()> { + // `list.insert` has special-case extra clamping logic for its index argument. + let index = { + if index < 0 { + // This can't exceed `isize::MAX` because `self.data[0]` is larger than a byte. + index += self.data.len() as isize; + } + if index < 0 { + 0 + } else if index as usize > self.data.len() { + self.data.len() + } else { + index as usize + } + }; + let py = value.py(); + let packed = self.pack(value)?; self.data.insert(index, packed); - if index == old_len { - self.update_param_table(py, old_len, None)?; + if index == self.data.len() - 1 { + self.update_param_table(py, index, None)?; } else { self.reindex_parameter_table(py)?; } Ok(()) } - pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { - let index = - index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); - let item = self.__getitem__(py, index.bind(py))?; - - self.__delitem__(py, index.bind(py).extract()?)?; + pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { + let index = index.unwrap_or(PySequenceIndex::Int(-1)); + let native_index = index.with_len(self.data.len())?; + let item = self.__getitem__(py, index)?; + self.delitem(py, native_index)?; Ok(item) } @@ -931,7 +878,7 @@ impl CircuitData { value: &Bound, params: Option)>>, ) -> PyResult { - let packed = self.pack(py, value.try_borrow()?)?; + let packed = self.pack(value.try_borrow()?)?; let new_index = self.data.len(); self.data.push(packed); self.update_param_table(py, new_index, params) @@ -1175,56 +1122,22 @@ impl CircuitData { } impl CircuitData { - /// Converts a Python slice to a `Vec` of indices into - /// the instruction listing, [CircuitData.data]. - fn convert_py_slice(&self, slice: &Bound) -> PyResult> { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - if indices.step > 0 { - Ok((indices.start..indices.stop) - .step_by(indices.step as usize) - .collect()) - } else { - let mut out = Vec::with_capacity(indices.slicelength as usize); - let mut x = indices.start; - while x > indices.stop { - out.push(x); - x += indices.step; - } - Ok(out) + /// Native internal driver of `__delitem__` that uses a Rust-space version of the + /// `SequenceIndex`. This assumes that the `SequenceIndex` contains only in-bounds indices, and + /// panics if not. + fn delitem(&mut self, py: Python, indices: SequenceIndex) -> PyResult<()> { + // We need to delete in reverse order so we don't invalidate higher indices with a deletion. + for index in indices.descending() { + self.data.remove(index); } - } - - /// Converts a Python index to an index into the instruction listing, - /// or one past its end. - /// If the resulting index would be < 0, clamps to 0. - /// If the resulting index would be > len(data), clamps to len(data). - fn convert_py_index_clamped(&self, index: isize) -> usize { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index - }; - std::cmp::min(std::cmp::max(0, index), self.data.len() as isize) as usize - } - - /// Converts a Python index to an index into the instruction listing. - fn convert_py_index(&self, index: isize) -> PyResult { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index - }; - - if index < 0 || index >= self.data.len() as isize { - return Err(PyIndexError::new_err(format!( - "Index {:?} is out of bounds.", - index, - ))); + if !indices.is_empty() { + self.reindex_parameter_table(py)?; } - Ok(index as usize) + Ok(()) } - fn pack(&mut self, py: Python, inst: PyRef) -> PyResult { + fn pack(&mut self, inst: PyRef) -> PyResult { + let py = inst.py(); let qubits = Interner::intern( &mut self.qargs_interner, InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 9fcaa36480c..9f0a8017bf2 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,23 +17,13 @@ pub mod gate_matrix; pub mod imports; pub mod operations; pub mod parameter_table; +pub mod slice; pub mod util; mod bit_data; mod interner; use pyo3::prelude::*; -use pyo3::types::PySlice; - -/// A private enumeration type used to extract arguments to pymethod -/// that may be either an index or a slice -#[derive(FromPyObject)] -pub enum SliceOrInt<'a> { - // The order here defines the order the variants are tried in the FromPyObject` derivation. - // `Int` is _much_ more common, so that should be first. - Int(isize), - Slice(Bound<'a, PySlice>), -} pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] diff --git a/crates/circuit/src/slice.rs b/crates/circuit/src/slice.rs new file mode 100644 index 00000000000..056adff0a28 --- /dev/null +++ b/crates/circuit/src/slice.rs @@ -0,0 +1,375 @@ +// 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 thiserror::Error; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::types::PySlice; + +use self::sealed::{Descending, SequenceIndexIter}; + +/// A Python-space indexer for the standard `PySequence` type; a single integer or a slice. +/// +/// These come in as `isize`s from Python space, since Python typically allows negative indices. +/// Use `with_len` to specialize the index to a valid Rust-space indexer into a collection of the +/// given length. +pub enum PySequenceIndex<'py> { + Int(isize), + Slice(Bound<'py, PySlice>), +} + +impl<'py> FromPyObject<'py> for PySequenceIndex<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + // `slice` can't be subclassed in Python, so it's safe (and faster) to check for it exactly. + // The `downcast_exact` check is just a pointer comparison, so while `slice` is the less + // common input, doing that first has little-to-no impact on the speed of the `isize` path, + // while the reverse makes `slice` inputs significantly slower. + if let Ok(slice) = ob.downcast_exact::() { + return Ok(Self::Slice(slice.clone())); + } + Ok(Self::Int(ob.extract()?)) + } +} + +impl<'py> PySequenceIndex<'py> { + /// Specialize this index to a collection of the given `len`, returning a Rust-native type. + pub fn with_len(&self, len: usize) -> Result { + match self { + PySequenceIndex::Int(index) => { + let index = if *index >= 0 { + let index = *index as usize; + if index >= len { + return Err(PySequenceIndexError::OutOfRange); + } + index + } else { + len.checked_sub(index.unsigned_abs()) + .ok_or(PySequenceIndexError::OutOfRange)? + }; + Ok(SequenceIndex::Int(index)) + } + PySequenceIndex::Slice(slice) => { + let indices = slice + .indices(len as ::std::os::raw::c_long) + .map_err(PySequenceIndexError::from)?; + if indices.step > 0 { + Ok(SequenceIndex::PosRange { + start: indices.start as usize, + stop: indices.stop as usize, + step: indices.step as usize, + }) + } else { + Ok(SequenceIndex::NegRange { + // `indices.start` can be negative if the collection length is 0. + start: (indices.start >= 0).then_some(indices.start as usize), + // `indices.stop` can be negative if the 0 index should be output. + stop: (indices.stop >= 0).then_some(indices.stop as usize), + step: indices.step.unsigned_abs(), + }) + } + } + } + } +} + +/// Error type for problems encountered when calling methods on `PySequenceIndex`. +#[derive(Error, Debug)] +pub enum PySequenceIndexError { + #[error("index out of range")] + OutOfRange, + #[error(transparent)] + InnerPy(#[from] PyErr), +} +impl From for PyErr { + fn from(value: PySequenceIndexError) -> PyErr { + match value { + PySequenceIndexError::OutOfRange => PyIndexError::new_err("index out of range"), + PySequenceIndexError::InnerPy(inner) => inner, + } + } +} + +/// Rust-native version of a Python sequence-like indexer. +/// +/// Typically this is constructed by a call to `PySequenceIndex::with_len`, which guarantees that +/// all the indices will be in bounds for a collection of the given length. +/// +/// This splits the positive- and negative-step versions of the slice in two so it can be translated +/// more easily into static dispatch. This type can be converted into several types of iterator. +#[derive(Clone, Copy, Debug)] +pub enum SequenceIndex { + Int(usize), + PosRange { + start: usize, + stop: usize, + step: usize, + }, + NegRange { + start: Option, + stop: Option, + step: usize, + }, +} + +impl SequenceIndex { + /// The number of indices this refers to. + pub fn len(&self) -> usize { + match self { + Self::Int(_) => 1, + Self::PosRange { start, stop, step } => { + let gap = stop.saturating_sub(*start); + gap / *step + (gap % *step != 0) as usize + } + Self::NegRange { start, stop, step } => 'arm: { + let Some(start) = start else { break 'arm 0 }; + let gap = stop + .map(|stop| start.saturating_sub(stop)) + .unwrap_or(*start + 1); + gap / step + (gap % step != 0) as usize + } + } + } + + pub fn is_empty(&self) -> bool { + // This is just to keep clippy happy; the length is already fairly inexpensive to calculate. + self.len() == 0 + } + + /// Get an iterator over the indices. This will be a single-item iterator for the case of + /// `Self::Int`, but you probably wanted to destructure off that case beforehand anyway. + pub fn iter(&self) -> SequenceIndexIter { + match self { + Self::Int(value) => SequenceIndexIter::Int(Some(*value)), + Self::PosRange { start, step, .. } => SequenceIndexIter::PosRange { + lowest: *start, + step: *step, + indices: 0..self.len(), + }, + Self::NegRange { start, step, .. } => SequenceIndexIter::NegRange { + // We can unwrap `highest` to an arbitrary value if `None`, because in that case the + // `len` is 0 and the iterator will not yield any objects. + highest: start.unwrap_or_default(), + step: *step, + indices: 0..self.len(), + }, + } + } + + // Get an iterator over the contained indices that is guaranteed to iterate from the highest + // index to the lowest. + pub fn descending(&self) -> Descending { + Descending(self.iter()) + } +} + +impl IntoIterator for SequenceIndex { + type Item = usize; + type IntoIter = SequenceIndexIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +// Private module to make it impossible to construct or inspect the internals of the iterator types +// from outside this file, while still allowing them to be used. +mod sealed { + /// Custom iterator for indices for Python sequence-likes. + /// + /// In the range types, the `indices ` are `Range` objects that run from 0 to the length of the + /// iterator. In theory, we could generate the iterators ourselves, but that ends up with a lot of + /// boilerplate. + #[derive(Clone, Debug)] + pub enum SequenceIndexIter { + Int(Option), + PosRange { + lowest: usize, + step: usize, + indices: ::std::ops::Range, + }, + NegRange { + highest: usize, + // The step of the iterator, but note that this is a negative range, so the forwards method + // steps downwards from `upper` towards `lower`. + step: usize, + indices: ::std::ops::Range, + }, + } + impl Iterator for SequenceIndexIter { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next().map(|idx| *highest - idx * *step), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Int(None) => (0, Some(0)), + Self::Int(Some(_)) => (1, Some(1)), + Self::PosRange { indices, .. } | Self::NegRange { indices, .. } => { + indices.size_hint() + } + } + } + } + impl DoubleEndedIterator for SequenceIndexIter { + #[inline] + fn next_back(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next_back().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next_back().map(|idx| *highest - idx * *step), + } + } + } + impl ExactSizeIterator for SequenceIndexIter {} + + pub struct Descending(pub SequenceIndexIter); + impl Iterator for Descending { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => self.0.next(), + SequenceIndexIter::PosRange { .. } => self.0.next_back(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + } + impl DoubleEndedIterator for Descending { + #[inline] + fn next_back(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => { + self.0.next_back() + } + SequenceIndexIter::PosRange { .. } => self.0.next(), + } + } + } + impl ExactSizeIterator for Descending {} +} + +#[cfg(test)] +mod test { + use super::*; + + /// Get a set of test parametrisations for iterator methods. The second argument is the + /// expected values from a normal forward iteration. + fn index_iterator_cases() -> impl Iterator)> { + let pos = |start, stop, step| SequenceIndex::PosRange { start, stop, step }; + let neg = |start, stop, step| SequenceIndex::NegRange { start, stop, step }; + + [ + (SequenceIndex::Int(3), vec![3]), + (pos(0, 5, 2), vec![0, 2, 4]), + (pos(2, 10, 1), vec![2, 3, 4, 5, 6, 7, 8, 9]), + (pos(1, 15, 3), vec![1, 4, 7, 10, 13]), + (neg(Some(3), None, 1), vec![3, 2, 1, 0]), + (neg(Some(3), None, 2), vec![3, 1]), + (neg(Some(2), Some(0), 1), vec![2, 1]), + (neg(Some(2), Some(0), 2), vec![2]), + (neg(Some(2), Some(0), 3), vec![2]), + (neg(Some(10), Some(2), 3), vec![10, 7, 4]), + (neg(None, None, 1), vec![]), + (neg(None, None, 3), vec![]), + ] + .into_iter() + } + + /// Test that the index iterator's implementation of `ExactSizeIterator` is correct. + #[test] + fn index_iterator() { + for (index, forwards) in index_iterator_cases() { + // We're testing that all the values are the same, and the `size_hint` is correct at + // every single point. + let mut actual = Vec::new(); + let mut sizes = Vec::new(); + let mut iter = index.iter(); + loop { + sizes.push(iter.size_hint().0); + if let Some(next) = iter.next() { + actual.push(next); + } else { + break; + } + } + assert_eq!( + actual, forwards, + "values for {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, forwards, + ); + let expected_sizes = (0..=forwards.len()).rev().collect::>(); + assert_eq!( + sizes, expected_sizes, + "sizes for {:?}\nActual : {:?}\nExpected: {:?}", + index, sizes, expected_sizes, + ); + } + } + + /// Test that the index iterator's implementation of `DoubleEndedIterator` is correct. + #[test] + fn reversed_index_iterator() { + for (index, forwards) in index_iterator_cases() { + let actual = index.iter().rev().collect::>(); + let expected = forwards.into_iter().rev().collect::>(); + assert_eq!( + actual, expected, + "reversed {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } + + /// Test that `descending` produces its values in reverse-sorted order. + #[test] + fn descending() { + for (index, mut expected) in index_iterator_cases() { + let actual = index.descending().collect::>(); + expected.sort_by(|left, right| right.cmp(left)); + assert_eq!( + actual, expected, + "descending {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } +} From 5deed7a73875277ff4e5669497d4ff1aa0abf785 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 1 Jul 2024 16:02:14 +0300 Subject: [PATCH 159/159] improving `quantum_causal_cone` method in python (#12668) * improving quantum_causal_cone * fixing release note --- qiskit/dagcircuit/dagcircuit.py | 60 +++++++----- ...-quantum-causal-cone-f63eaaa9ab658811.yaml | 5 + test/python/dagcircuit/test_dagcircuit.py | 97 +++++++++++++++++++ 3 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index d14340a8cb9..626b7ef053e 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -2264,36 +2264,44 @@ def quantum_causal_cone(self, qubit): output_node = self.output_map.get(qubit, None) if not output_node: raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - # Add the qubit to the causal cone. - qubits_to_check = {qubit} - # Add predecessors of output node to the queue. - queue = deque(self.predecessors(output_node)) - # While queue isn't empty + qubits_in_cone = {qubit} + queue = deque(self.quantum_predecessors(output_node)) + + # The processed_non_directive_nodes stores the set of processed non-directive nodes. + # This is an optimization to avoid considering the same non-directive node multiple + # times when reached from different paths. + # The directive nodes (such as barriers or measures) are trickier since when processing + # them we only add their predecessors that intersect qubits_in_cone. Hence, directive + # nodes have to be considered multiple times. + processed_non_directive_nodes = set() + while queue: - # Pop first element. node_to_check = queue.popleft() - # Check whether element is input or output node. + if isinstance(node_to_check, DAGOpNode): - # Keep all the qubits in the operation inside a set. - qubit_set = set(node_to_check.qargs) - # Check if there are any qubits in common and that the operation is not a barrier. - if ( - len(qubit_set.intersection(qubits_to_check)) > 0 - and node_to_check.op.name != "barrier" - and not getattr(node_to_check.op, "_directive") - ): - # If so, add all the qubits to the causal cone. - qubits_to_check = qubits_to_check.union(qubit_set) - # For each predecessor of the current node, filter input/output nodes, - # also make sure it has at least one qubit in common. Then append. - for node in self.quantum_predecessors(node_to_check): - if ( - isinstance(node, DAGOpNode) - and len(qubits_to_check.intersection(set(node.qargs))) > 0 - ): - queue.append(node) - return qubits_to_check + # If the operation is not a directive (in particular not a barrier nor a measure), + # we do not do anything if it was already processed. Otherwise, we add its qubits + # to qubits_in_cone, and append its predecessors to queue. + if not getattr(node_to_check.op, "_directive"): + if node_to_check in processed_non_directive_nodes: + continue + qubits_in_cone = qubits_in_cone.union(set(node_to_check.qargs)) + processed_non_directive_nodes.add(node_to_check) + for pred in self.quantum_predecessors(node_to_check): + if isinstance(pred, DAGOpNode): + queue.append(pred) + else: + # Directives (such as barriers and measures) may be defined over all the qubits, + # yet not all of these qubits should be considered in the causal cone. So we + # only add those predecessors that have qubits in common with qubits_in_cone. + for pred in self.quantum_predecessors(node_to_check): + if isinstance(pred, DAGOpNode) and not qubits_in_cone.isdisjoint( + set(pred.qargs) + ): + queue.append(pred) + + return qubits_in_cone def properties(self): """Return a dictionary of circuit properties.""" diff --git a/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml new file mode 100644 index 00000000000..5a072f481ab --- /dev/null +++ b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml @@ -0,0 +1,5 @@ +--- +features_circuits: + - | + Improved performance of the method :meth:`.DAGCircuit.quantum_causal_cone` by not examining + the same non-directive node multiple times when reached from different paths. diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 4ab4e392cbb..3e4d4bf4e68 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -3475,6 +3475,103 @@ def test_causal_cone_barriers(self): self.assertEqual(result, expected) + def test_causal_cone_more_barriers(self): + """Test causal cone for circuit with barriers. This example shows + why barriers may need to be examined multiple times.""" + + # q0_0: ──■────────░──────────────────────── + # ┌─┴─┐ ░ + # q0_1: ┤ X ├──────░───■──────────────────── + # ├───┤ ░ ┌─┴─┐┌───┐┌───┐┌───┐ + # q0_2: ┤ H ├──────░─┤ X ├┤ H ├┤ H ├┤ H ├─X─ + # ├───┤┌───┐ ░ └───┘└───┘└───┘└───┘ │ + # q0_3: ┤ H ├┤ X ├─░──────────────────────X─ + # ├───┤└─┬─┘ ░ + # q0_4: ┤ X ├──■───░──────────────────────── + # └─┬─┘ ░ + # q0_5: ──■────────░──────────────────────── + + qreg = QuantumRegister(6) + qc = QuantumCircuit(qreg) + qc.cx(0, 1) + qc.h(2) + qc.cx(5, 4) + qc.h(3) + qc.cx(4, 3) + qc.barrier() + qc.cx(1, 2) + + qc.h(2) + qc.h(2) + qc.h(2) + qc.swap(2, 3) + + dag = circuit_to_dag(qc) + + result = dag.quantum_causal_cone(qreg[2]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3], qreg[4], qreg[5]} + + self.assertEqual(result, expected) + + def test_causal_cone_measure(self): + """Test causal cone with measures.""" + + # ┌───┐ ░ ┌─┐ + # q_0: ┤ H ├─░─┤M├──────────── + # ├───┤ ░ └╥┘┌─┐ + # q_1: ┤ H ├─░──╫─┤M├───────── + # ├───┤ ░ ║ └╥┘┌─┐ + # q_2: ┤ H ├─░──╫──╫─┤M├────── + # ├───┤ ░ ║ ║ └╥┘┌─┐ + # q_3: ┤ H ├─░──╫──╫──╫─┤M├─── + # ├───┤ ░ ║ ║ ║ └╥┘┌─┐ + # q_4: ┤ H ├─░──╫──╫──╫──╫─┤M├ + # └───┘ ░ ║ ║ ║ ║ └╥┘ + # c: 5/═════════╬══╬══╬══╬══╬═ + # ║ ║ ║ ║ ║ + # meas: 5/═════════╩══╩══╩══╩══╩═ + # 0 1 2 3 4 + + qreg = QuantumRegister(5) + creg = ClassicalRegister(5) + circuit = QuantumCircuit(qreg, creg) + for i in range(5): + circuit.h(i) + circuit.measure_all() + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[1]} + self.assertEqual(result, expected) + + def test_reconvergent_paths(self): + """Test circuit with reconvergent paths.""" + + # q0_0: ──■─────────■─────────■─────────■─────────■─────────■─────── + # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ + # q0_1: ┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■── + # └───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐ + # q0_2: ──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├ + # ┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘ + # q0_3: ┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├───── + # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + # q0_4: ──────────────────────────────────────────────────────────── + + qreg = QuantumRegister(5) + circuit = QuantumCircuit(qreg) + + for _ in range(6): + circuit.cx(0, 1) + circuit.cx(2, 3) + circuit.cx(1, 2) + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3]} + self.assertEqual(result, expected) + if __name__ == "__main__": unittest.main()
' % result + ret = f'
{title} ' + ret += f'
' ret += "
" return ret @@ -119,11 +119,7 @@ def diff_images(self): if os.path.exists(os.path.join(SWD, fullpath_reference)): ratio, diff_name = Results._similarity_ratio(fullpath_name, fullpath_reference) - title = "{} | {} | ratio: {}".format( - name, - self.data[name]["testname"], - ratio, - ) + title = f"{name} | {self.data[name]['testname']} | ratio: {ratio}" if ratio == 1: self.exact_match.append(fullpath_name) else: @@ -158,8 +154,9 @@ def _repr_html_(self): ) else: title = ( - 'Download this image to %s' - " and add/push to the repo